/** * Built-in Breadcrumbs handle for accumulating breadcrumb items across route segments. * * Each layout/route pushes breadcrumb items via `ctx.use(Breadcrumbs)`. * Items are collected in parent-to-child order with automatic deduplication * by `href` (last item for each href wins). * * @example * ```tsx * // In route handler * route("/blog/:slug", (ctx) => { * const breadcrumb = ctx.use(Breadcrumbs); * breadcrumb({ label: "Blog", href: "/blog" }); * breadcrumb({ label: post.title, href: `/blog/${ctx.params.slug}` }); * }); * * // In client component (consume with useHandle) * const crumbs = useHandle(Breadcrumbs); * crumbs.map((c) => {c.label}); * ``` */ import type { ReactNode } from "react"; import { createHandle, type Handle } from "../handle.js"; /** * A single breadcrumb item. * * @property label - Display text for the breadcrumb * @property href - URL the breadcrumb links to * @property content - Optional extra content (sync or async) rendered alongside the label */ export interface BreadcrumbItem { label: string; href: string; content?: ReactNode | Promise; } /** * Collect function for Breadcrumbs handle. * Flattens segments in parent-to-child order with deduplication by href * (last item for each href wins). */ function collectBreadcrumbs(segments: BreadcrumbItem[][]): BreadcrumbItem[] { const all = segments.flat(); const seen = new Map(); for (let i = 0; i < all.length; i++) { seen.set(all[i].href, i); } // Return items in order, keeping only the last occurrence per href return all.filter((item, index) => seen.get(item.href) === index); } /** * Built-in handle for accumulating breadcrumb navigation items. * * Use `ctx.use(Breadcrumbs)` in route handlers to push breadcrumb items. * Use `useHandle(Breadcrumbs)` in client components to consume them. */ export const Breadcrumbs: Handle = createHandle( collectBreadcrumbs, "__rsc_router_breadcrumbs__", );