# Blog Index + Search + Breadcrumb — Design Spec

- **Date**: 2026-06-05
- **Branch**: `feat/blog-index-search` (from `dev`)
- **Status**: approved design, pending implementation plan
- **Builds on**: `2026-06-05-mdx-blog-design.md` (the MDX blog, already merged to `dev`)

## Goal

Replace the per-category index pages with a single **all-articles index at `/{locale}/blog`** featuring client-side fuzzy search (Fuse.js) + category filter, and add a **visible breadcrumb** (shadcn `@/components/ui/breadcrumb`) to articles and the index. Article URLs (`/{locale}/{category}/{slug}`) are unchanged.

## Decisions (locked)

1. Single index at `/{locale}/blog` (slug `/blog` in both en and fr). Remove the 6 category index pages.
2. Article URLs unchanged (`/{locale}/{category}/{slug}`); `/{category}` alone → 404 (via existing `[...rest]`).
3. Search: **Fuse.js** (fuzzy), client-side over post metadata passed as props. Category filter chips alongside.
4. Breadcrumb: the existing shadcn `src/components/ui/breadcrumb.tsx`, used on articles (Home › Category › Title) and on `/blog` (Home › Blog). The category crumb links to `/blog?category=<localized-slug>`.
5. `localePrefix: "as-needed"` respected for all new URLs (EN unprefixed).
6. Out of scope: pagination, server-side search, sort controls (date-desc default).

## Routing changes

- `src/i18n/routing.ts`: add `"/blog": { en: "/blog", fr: "/blog" }`.
- **Create** `src/app/[locale]/blog/page.tsx` — Server Component. Fetches `getAllPosts(locale)`, maps to a serializable DTO array, renders header + `<BlogSearch posts={...} />`. `generateStaticParams` over locales; localized metadata with canonical + hreflang (via `getPathname`/`localePrefix`). JSON-LD: `WebPage` + `BreadcrumbList` (Home › Blog).
- **Delete** `src/app/[locale]/[category]/page.tsx` and its sibling `loading.tsx` / `error.tsx` / `not-found.tsx` (category-index-only). Keep `src/app/[locale]/[category]/[slug]/**` (articles) and its boundary files. `/{category}` with no page falls through to `[...rest]` → `notFound()`.
- **Delete** `src/components/blog/CategoryNav.tsx` (it linked to the removed category indexes). Its role is replaced by the filter chips inside `BlogSearch`.

## Index page

### DTO (server → client, serializable)
```ts
type BlogListItem = {
  postId: string;
  title: string;
  description: string;
  category: CategoryId;
  categoryLabel: string;   // localized (t(`categories.${id}`))
  href: string;            // as-needed path: /{cat}/{slug} or /fr/{cat}/{slug}
  date: string;            // ISO
  readingMinutes: number;
  cover: string;
  coverAlt: string;
};
```
Built in the server page from `getAllPosts(locale)` + `categoryLabel` from `getTranslations`.

### `BlogSearch` (client component)
- `"use client"`. Props: `items: BlogListItem[]`.
- Fuse.js instance memoized over `items`, keys `["title", "description", "categoryLabel"]`, `threshold: 0.3`, `ignoreLocation: true`.
- State: `query` (text), `activeCategory` (`CategoryId | "all"`). Initial `activeCategory` from `useSearchParams().get("category")` mapped via `categoryIdFromSlug` (fallback "all").
- Derived list: start from `items`; if `query` non-empty → Fuse search; then filter by `activeCategory` (unless "all"); preserve date-desc.
- UI: search `<input>` (machined/brand style, label via i18n), filter chips (All + 6 categories, tokens, active = teal border), `PostCard` grid (reused), empty-state (`search.noResults`).
- No URL writeback required (reading `?category=` for the breadcrumb deep-link is enough); optional `history.replaceState` is out of scope.

### `PostCard`
Reused as-is. (It builds its own localized href via next-intl `Link` + `categorySlug`; the DTO `href` is for non-card uses — keep `PostCard` consuming `PostMeta`-shaped fields it already uses, or adapt to `BlogListItem`. Plan picks one; keep a single card prop shape.)

## Breadcrumb

- Use `src/components/ui/breadcrumb.tsx` (`Breadcrumb`, `BreadcrumbList`, `BreadcrumbItem`, `BreadcrumbLink`, `BreadcrumbSeparator`, `BreadcrumbPage`).
- `BreadcrumbLink asChild` wrapping next-intl `Link` for internal localized links (Home `/`, category `/blog?category=<slug>`).
- **Article** (`PostLayout`, above the header): Home › {categoryLabel → `/blog?category=<localized slug>`} › {post.title as `BreadcrumbPage`}.
- **/blog**: Home › Blog (`BreadcrumbPage`).
- Brand: tokens only; if shadcn defaults clash (rounded, colors), override with token classes. Keep the existing JSON-LD `BreadcrumbList` on articles (now mirrors the visible trail); add the same to `/blog`.
- i18n: `BlogPage.breadcrumbHome` ("Home"/"Accueil") or reuse an existing home label.

## Dependencies

- `sfw bun add fuse.js`; add `"fuse.js"` to `experimental.optimizePackageImports` in `next.config.ts`.

## i18n (`BlogPage`, en/fr)

Add: `indexTitle` ("Blog"/"Blog"), `indexIntro` (one line), `search.placeholder` ("Search articles…"/"Rechercher un article…"), `search.noResults`, `search.allCategories` (or reuse `allCategories`), `breadcrumbHome`. Reuse existing `categories.*`, `readingTime`, etc.

## Sitemap

`src/app/sitemap.ts`: remove the 6 category-index entries; add the `/blog` index (both locales, hreflang + x-default). Article + feed entries unchanged.

## Success criteria

- `bun run lint` clean; `bun run build` green; `/blog` + `/fr/blog` prerendered (the search/filter is client-side within the static shell); articles still SSG.
- `/{category}` (e.g. `/sourcing`) returns 404; `/{category}/{slug}` still works.
- Search filters by title/description/category (fuzzy); `?category=` deep-link pre-selects a chip.
- Visible breadcrumb on articles (Home › Category › Title) and `/blog`; category crumb deep-links to the filtered index; JSON-LD BreadcrumbList present on both.
- Sitemap lists `/blog` (both locales, hreflang), no stale category-index URLs.

## Open items (deferred)

- URL writeback of active filters (shareable filtered URLs beyond the initial `?category=`).
- Visual polish of the index/search/breadcrumb (separate design pass).
