Skip to content

Breadcrumbs, chips & flavour taxonomy

Three small pieces that, together, give Google many more crawl paths and let shoppers cross-navigate the catalogue by family or flavour profile.

When visible breadcrumbs render

A visible HTML breadcrumb trail sits directly under <Header /> on every deep page. The component is markup-only — it never emits JSON-LD. Structured BreadcrumbList schema stays owned by each page via breadcrumbSchema() in lib/seo/structured-data.ts, so search engines see exactly one schema entry.

Page Trail
/p/{slug} Home › Shop › {Category} › {Product}
/fruit/{slug} Home › The Fruit Atlas › {Family} › {Fruit}
/atlas/{slug} Home › The Fruit Atlas › {Family} (replaces the old thin nav)
/calendar Home › Seasonal calendar
/b/{slug} Home › Saved boxes ›
/t/{tag} Home › The Fruit Atlas › {Tag} fruits

The component — apps/web/components/seo/Breadcrumbs.tsx — renders a <nav aria-label="Breadcrumb"> with an <ol>, uses lucide ChevronRight at strokeWidth={1.75} for separators, and marks the last item with aria-current="page". It uses typedRoutes (as Route) so the tsc build catches any broken link.

How chips work

Family chips and flavour chips render as small clickable pills:

  • Family chip (sticker-amber accent) links to /atlas/{family-key}.
  • Flavour chips (magenta primary accent) link to /t/{flavour-slug}, where the slug is the flavour name lowercased and hyphenated.

Chips are live on:

  • the PDP (/p/{slug}) — replaces the previous plain pill strip
  • the preparation guide (/fruit/{slug}) — sits above the blurb
  • the Atlas FruitCard (/atlas/{slug}) — uses a stacked-link pattern so the flavour chips are real anchors and the card still click-throughs to the PDP / prep guide

Never nest an <a> inside another <a>. The pattern we use: make the card container relative, give the title a <Link> with an absolute-inset overlay span, and float the chip row at z-10 so its links stay clickable.

/t/{tag} — flavour landing pages

Every flavour that shows up in at least one fruit's flavor[] array gets a statically-generated landing page:

  • generateStaticParams() reads tagSlugs() from lib/tags.ts
  • generateMetadata() emits title "{Tag} fruits — Fruit Plug", description "Every fruit we source with a {tag} flavour profile.", canonical URL, and full OG/Twitter cards
  • JSON-LD: BreadcrumbList + CollectionPage + ItemList
  • Hero: kicker "FLAVOUR PROFILE", h1 in fp-display, one-line sub
  • Grid: every fruit whose flavor[] contains this tag, using the local watermarked PNG when available; links to the PDP when Woo has a product, else the prep guide (via fruitHref(slug, wooSlugs))
  • "Related flavours" tail: the 4 tags that most frequently co-occur with this one, computed on the fly by relatedTags(tag, 4)
  • Every /t/{tag} URL is registered in apps/web/app/sitemap.ts at priority 0.6

Helpers — lib/tags.ts

tagSlug(tag)        // "Sweet" → "sweet"
tagLabel(tag)       // "sweet" → "Sweet"
tagSlugs()          // all distinct slugs used in FRUITS
fruitsByTag(tag)    // FruitRecord[] — stable FRUITS order
relatedTags(tag, n) // top-N co-occurring tags (excluding tag itself)

The file stays under 40 lines on purpose — everything bigger belongs in the taxonomy itself.