Barazo default frontend barazo.forum
at main 88 lines 3.0 kB view raw
1/** 2 * Breadcrumbs component with JSON-LD structured data. 3 * WCAG 2.2 AA: nav landmark, semantic list, 44px mobile touch target. 4 * Mobile: collapses to single parent back-link. 5 * Desktop: full breadcrumb trail. 6 * @see https://schema.org/BreadcrumbList 7 */ 8 9import Link from 'next/link' 10import { CaretLeft } from '@phosphor-icons/react/dist/ssr' 11 12export interface BreadcrumbItem { 13 label: string 14 href?: string 15} 16 17interface BreadcrumbsProps { 18 items: BreadcrumbItem[] 19 /** When provided, JSON-LD uses these instead of `items`. Lets pages keep full path in structured data while showing fewer visual breadcrumbs. */ 20 jsonLdItems?: BreadcrumbItem[] 21} 22 23export function Breadcrumbs({ items, jsonLdItems }: BreadcrumbsProps) { 24 if (items.length === 0) return null 25 26 const jsonLdSource = jsonLdItems ?? items 27 const jsonLd = { 28 '@context': 'https://schema.org', 29 '@type': 'BreadcrumbList', 30 itemListElement: jsonLdSource 31 .filter((item) => item.href) 32 .map((item, index) => ({ 33 '@type': 'ListItem', 34 position: index + 1, 35 name: item.label, 36 item: `https://barazo.forum${item.href}`, 37 })), 38 } 39 40 // Last item with an href = parent link for mobile back-link 41 const parentItem = [...items].reverse().find((item) => item.href) 42 43 return ( 44 <nav aria-label="Breadcrumb" className="mb-4"> 45 <script 46 type="application/ld+json" 47 dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} 48 /> 49 50 {/* Mobile: single parent back-link */} 51 {parentItem && ( 52 <Link 53 href={parentItem.href!} 54 className="flex min-h-[44px] items-center gap-1 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded-sm md:hidden" 55 > 56 <CaretLeft size={14} aria-hidden="true" /> 57 {parentItem.label} 58 </Link> 59 )} 60 61 {/* Desktop: full breadcrumb trail */} 62 <ol className="hidden items-center gap-1 text-sm text-muted-foreground md:flex"> 63 {items.map((item, index) => { 64 const isLast = index === items.length - 1 65 return ( 66 <li key={item.href ?? item.label} className="flex items-center gap-1"> 67 {index > 0 && ( 68 <span aria-hidden="true" className="text-muted-foreground"> 69 / 70 </span> 71 )} 72 {isLast || !item.href ? ( 73 <span className={isLast ? 'font-medium text-foreground' : ''}>{item.label}</span> 74 ) : ( 75 <Link 76 href={item.href} 77 className="transition-colors hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded-sm px-1" 78 > 79 {item.label} 80 </Link> 81 )} 82 </li> 83 ) 84 })} 85 </ol> 86 </nav> 87 ) 88}