Skip to content

Commit f6a37bd

Browse files
nikoshellegeakman
andauthored
Add breadcrumbs (#1451)
Fix #1009 <img width="928" height="580" alt="image" src="https://github.com/user-attachments/assets/49df4a18-742f-43db-8ba0-fabca3399074" /> --------- Co-authored-by: Ege Akman <[email protected]>
1 parent ab0d1a9 commit f6a37bd

File tree

2 files changed

+138
-0
lines changed

2 files changed

+138
-0
lines changed

src/components/Breadcrumbs.astro

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
---
2+
import Section from "@ui/Section.astro";
3+
4+
export interface Props {
5+
linksData: any;
6+
currentPath: string;
7+
homeLabel?: string;
8+
homePath?: string;
9+
separator?: string;
10+
}
11+
12+
const {
13+
linksData,
14+
currentPath,
15+
homeLabel = "Home",
16+
homePath = "/",
17+
separator = "/"
18+
} = Astro.props;
19+
20+
interface BreadcrumbItem {
21+
name: string;
22+
path: string;
23+
isActive: boolean;
24+
}
25+
26+
function normalizePath(path: string): string {
27+
console.log(path)
28+
if (!path) return ""
29+
if (path === "/") return "/";
30+
return path.replace(/\/+$/, "");
31+
}
32+
33+
const normalizedCurrentPath = normalizePath(currentPath);
34+
35+
// Recursive function to build breadcrumb trail
36+
function findBreadcrumbTrail(items: any[], currentPath: string, parentName?: string): BreadcrumbItem[] {
37+
for (const item of items) {
38+
const itemPath = normalizePath(item.path);
39+
40+
if (itemPath === currentPath) {
41+
const breadcrumb: BreadcrumbItem = {
42+
name: item.name,
43+
path: item.path,
44+
isActive: true
45+
};
46+
47+
if (parentName) {
48+
return [
49+
{ name: parentName, path: '', isActive: false },
50+
breadcrumb
51+
];
52+
}
53+
54+
return [breadcrumb];
55+
}
56+
57+
if (item.items) {
58+
const subTrail = findBreadcrumbTrail(item.items, currentPath, item.name);
59+
if (subTrail.length > 0) {
60+
return subTrail;
61+
}
62+
}
63+
}
64+
65+
return [];
66+
}
67+
68+
// Build the breadcrumb trail
69+
const breadcrumbs: BreadcrumbItem[] = [];
70+
71+
breadcrumbs.push({
72+
name: homeLabel,
73+
path: homePath,
74+
isActive: normalizedCurrentPath === normalizePath(homePath)
75+
});
76+
77+
if (normalizedCurrentPath !== normalizePath(homePath) && linksData?.header) {
78+
const trail = findBreadcrumbTrail(linksData.header, normalizedCurrentPath);
79+
breadcrumbs.push(...trail);
80+
}
81+
82+
if (breadcrumbs.length === 1 && normalizedCurrentPath !== normalizePath(homePath)) {
83+
const pathSegments = normalizedCurrentPath.split('/').filter(Boolean);
84+
const lastSegment = pathSegments[pathSegments.length - 1];
85+
86+
if (lastSegment) {
87+
breadcrumbs.push({
88+
name: lastSegment.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
89+
path: currentPath,
90+
isActive: true
91+
});
92+
}
93+
}
94+
---
95+
96+
<Section>
97+
{ currentPath !== homePath &&
98+
<nav aria-label="Breadcrumb" class="px-4">
99+
<ol class="flex flex-wrap items-center text-sm list-none m-0 p-0">
100+
{breadcrumbs.map((crumb, index) => (
101+
<li class="flex items-center">
102+
{crumb.isActive ? (
103+
<span class="text-gray-900 font-medium px-2 py-1" aria-current="page">
104+
{crumb.name}
105+
</span>
106+
) : crumb.path ? (
107+
<a href={crumb.path} class="underline hover:text-button-hover px-2 py-1 rounded transition-colors duration-200 hover:underline">
108+
{crumb.name}
109+
</a>
110+
) : (
111+
<span class="text-gray-500 italic px-2 py-1">
112+
{crumb.name}
113+
</span>
114+
)}
115+
{index < breadcrumbs.length - 1 && (
116+
<span class="mx-2 text-gray-400 select-none" aria-hidden="true">
117+
{separator}
118+
</span>
119+
)}
120+
</li>
121+
))}
122+
</ol>
123+
</nav>
124+
}
125+
</Section>

src/layouts/Layout.astro

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@ import BaseHead from "@components/BaseHead.astro";
33
import Header from "@components/Header.astro";
44
import Footer from "@components/Footer.astro";
55
import Offline from "@components/Offline.astro";
6+
import Breadcrumbs from '@components/Breadcrumbs.astro';
7+
8+
import linksData from '@src/data/links.json';
69
710
import "@fortawesome/fontawesome-free/css/all.min.css";
811
import "@styles/tailwind.css";
912
import "@styles/global.css";
1013
import "@styles/search.css";
1114
15+
1216
export interface Props {
1317
title: string;
1418
description: string;
1519
}
1620
21+
const currentPath = Astro.url.pathname;
22+
1723
const { title, description } = Astro.props;
1824
1925
if (!title || !description) {
@@ -37,6 +43,13 @@ const externalDomain = new URL(Astro.site || "").hostname;
3743

3844
<main class="main pt-28" role="main">
3945
<Offline />
46+
<Breadcrumbs
47+
linksData={linksData}
48+
currentPath={currentPath}
49+
homeLabel="EuroPython 2025"
50+
homePath="/"
51+
separator=""
52+
/>
4053
<slot />
4154
</main>
4255

0 commit comments

Comments
 (0)