1+ 'use client'
2+
3+ import { useEffect , useRef , useState } from 'react'
14import { JsonLd } from '@/components/JsonLd'
25import { Link } from '@/i18n/navigation'
36import { SITE_CONFIG } from '@/lib/metadata/config'
@@ -10,17 +13,33 @@ export interface BreadcrumbItem {
1013/**
1114 * Renders a breadcrumb navigation bar and injects a Schema.org BreadcrumbList via JsonLd.
1215 * - Visual trail is rendered from provided items; the last item is shown as plain text.
13- * - Structured data includes an optional "Home" at the first position for better SEO.
16+ * - Structured data includes "Home" at the first position for better SEO.
17+ * - Sticky behavior is enabled when scrolling.
1418 */
15- export function Breadcrumb ( {
16- items,
17- siteOrigin = SITE_CONFIG . url ,
18- includeHome = true ,
19- } : {
20- items : BreadcrumbItem [ ]
21- siteOrigin ?: string
22- includeHome ?: boolean
23- } ) {
19+ export function Breadcrumb ( { items } : { items : BreadcrumbItem [ ] } ) {
20+ const breadcrumbRef = useRef < HTMLDivElement > ( null )
21+ const [ isBreadcrumbFixed , setIsBreadcrumbFixed ] = useState ( false )
22+ const [ headerHeight , setHeaderHeight ] = useState ( 0 )
23+
24+ useEffect ( ( ) => {
25+ const handleScroll = ( ) => {
26+ if ( ! breadcrumbRef . current ) return
27+ const header = document . querySelector ( 'header' )
28+ const currentHeaderHeight = header ?. offsetHeight || 0
29+ setHeaderHeight ( currentHeaderHeight )
30+ const breadcrumbTop = breadcrumbRef . current . offsetTop
31+ setIsBreadcrumbFixed ( window . scrollY > breadcrumbTop - currentHeaderHeight )
32+ }
33+
34+ window . addEventListener ( 'scroll' , handleScroll )
35+ window . addEventListener ( 'resize' , handleScroll )
36+ handleScroll ( )
37+
38+ return ( ) => {
39+ window . removeEventListener ( 'scroll' , handleScroll )
40+ window . removeEventListener ( 'resize' , handleScroll )
41+ }
42+ } , [ ] )
2443 // Normalize href to ensure it starts with '/' (unless it's already an absolute URL)
2544 const normalizeHref = ( href : string ) : string => {
2645 // If it's already an absolute URL or starts with '/', return as is
@@ -32,23 +51,19 @@ export function Breadcrumb({
3251 }
3352
3453 const schemaItems = [
35- ...( includeHome
36- ? [
37- {
38- '@type' : 'ListItem' ,
39- position : 1 ,
40- name : 'Home' ,
41- item : `${ siteOrigin } ` ,
42- } ,
43- ]
44- : [ ] ) ,
54+ {
55+ '@type' : 'ListItem' ,
56+ position : 1 ,
57+ name : 'Home' ,
58+ item : SITE_CONFIG . url ,
59+ } ,
4560 ...items . map ( ( item , index ) => {
4661 const normalizedHref = normalizeHref ( item . href )
4762 return {
4863 '@type' : 'ListItem' ,
49- position : ( includeHome ? 2 : 1 ) + index ,
64+ position : 2 + index ,
5065 name : item . name ,
51- item : `${ siteOrigin } ${ normalizedHref } ` ,
66+ item : `${ SITE_CONFIG . url } ${ normalizedHref } ` ,
5267 }
5368 } ) ,
5469 ]
@@ -59,37 +74,51 @@ export function Breadcrumb({
5974 itemListElement : schemaItems ,
6075 } as const
6176
77+ const BreadcrumbContent = ( ) => (
78+ < section
79+ className = "py-[var(--spacing-sm)] bg-[var(--color-hover)] border-b border-[var(--color-border)]"
80+ data-breadcrumb
81+ >
82+ < div className = "max-w-8xl mx-auto px-[var(--spacing-md)]" >
83+ < nav className = "flex items-center gap-[var(--spacing-xs)] text-sm pl-[var(--spacing-xs)]" >
84+ { items . map ( ( item , index ) => {
85+ const isLast = index === items . length - 1
86+ const normalizedHref = normalizeHref ( item . href )
87+ return (
88+ < span
89+ key = { `${ item . href } -${ index } ` }
90+ className = "inline-flex items-center gap-[var(--spacing-xs)]"
91+ >
92+ { isLast ? (
93+ < span className = "text-[var(--color-text)] font-medium" > { item . name } </ span >
94+ ) : (
95+ < Link
96+ href = { normalizedHref }
97+ className = "text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
98+ >
99+ { item . name }
100+ </ Link >
101+ ) }
102+ { ! isLast && < span className = "text-[var(--color-text-muted)]" > /</ span > }
103+ </ span >
104+ )
105+ } ) }
106+ </ nav >
107+ </ div >
108+ </ section >
109+ )
110+
62111 return (
63112 < >
64113 < JsonLd data = { breadcrumbListSchema } />
65- < section className = "py-[var(--spacing-sm)] bg-[var(--color-hover)] border-b border-[var(--color-border)]" >
66- < div className = "max-w-8xl mx-auto px-[var(--spacing-md)]" >
67- < nav className = "flex items-center gap-[var(--spacing-xs)] text-[0.8125rem]" >
68- { items . map ( ( item , index ) => {
69- const isLast = index === items . length - 1
70- const normalizedHref = normalizeHref ( item . href )
71- return (
72- < span
73- key = { `${ item . href } -${ index } ` }
74- className = "inline-flex items-center gap-[var(--spacing-xs)]"
75- >
76- { isLast ? (
77- < span className = "text-[var(--color-text)] font-medium" > { item . name } </ span >
78- ) : (
79- < Link
80- href = { normalizedHref }
81- className = "text-[var(--color-text-secondary)] hover:text-[var(--color-text)] transition-colors"
82- >
83- { item . name }
84- </ Link >
85- ) }
86- { ! isLast && < span className = "text-[var(--color-text-muted)]" > /</ span > }
87- </ span >
88- )
89- } ) }
90- </ nav >
114+ { isBreadcrumbFixed && (
115+ < div className = "fixed left-0 right-0 z-40 shadow-sm" style = { { top : `${ headerHeight } px` } } >
116+ < BreadcrumbContent />
91117 </ div >
92- </ section >
118+ ) }
119+ < div ref = { breadcrumbRef } className = { isBreadcrumbFixed ? 'invisible' : '' } >
120+ < BreadcrumbContent />
121+ </ div >
93122 </ >
94123 )
95124}
0 commit comments