Skip to content

Preparation guides — /fruit/[slug]

Public, SEO-friendly content pages for every fruit in the taxonomy. They sit alongside the Woo PDP (/p/[slug]) and the Atlas family page (/atlas/[slug]) — same data, different intent. The PDP sells a SKU; the prep guide ranks for "how to eat X" queries and converts curiosity into a basket add.

Routes

Route Purpose
/fruit/[slug] Preparation guide per fruit. Static-rendered from the taxonomy.
/atlas Botanical family index.
/atlas/[slug] Family page. Each fruit card now routes via fruit-routes.ts.

Every fruit in apps/web/lib/fruit-taxonomy.ts gets a guide automatically — generateStaticParams() derives the list from FRUITS.

Routing helper

apps/web/lib/fruit-routes.ts exports two functions used wherever we link to a fruit from another surface:

wooSlugSet(slugs: Iterable<string>): Set<string>
fruitHref(slug: string, wooSlugs: Set<string>): Route
hasPdp(slug: string, wooSlugs: Set<string>): boolean

The decision is binary: if a Woo product with that slug exists, we link to the PDP; otherwise we fall back to the prep guide. The Atlas family page fetches the full Woo product list once per request, builds the Set, and passes it to every FruitCard. The prep guide does the same to label its "Buy this fruit" CTA — /p/{slug} when a product exists, otherwise /shop?search={name}.

Page anatomy

Top to bottom:

  1. Cinematic hero — local 2048×2048 PNG from /public/media/fruits/, resolved via lib/fruit-images.ts. Radial vignette + bottom gradient, centered title + scientific name + family link.
  2. Availability strip — green when in season, amber on the shoulder, neutral when out of season or unrecognised. Parsed from the season_uk field with a tiny month parser (see below).
  3. Long-form content — the 45–60 word blurb as a lede, then the ~150-word description from the taxonomy, sectioned by paragraph. Verbatim — we never re-author the per-fruit copy.
  4. Did you know? — three Wikipedia-sourced facts per fruit (origin / cultivars & relatives / quirky detail), rendered as bullet points with a Wikipedia source link. Data lives in apps/web/lib/fruit-wiki-facts.ts (a sibling of the taxonomy file — the taxonomy itself stays untouched). The raw fetch output is cached at reports/wiki-fact-cache-2026-04-25.json so re-runs don't re-hit Wikipedia. Missing slugs skip the section silently.
  5. How to eat — generic, four-step prep block (ripeness → wash → cut → pair). Deliberately untethered to any specific cultivar so we never invent facts. The family-level tip from FAMILIES[fam].tip closes the section.
  6. Buy CTA — primary action plus saleFromCost(cost_gbp) price when the taxonomy carries a cost.
  7. Sidebar — family link, fact dl (origin / season / flavour), similar fruits in the same family, back-to-Atlas.

Heading order

Strict h1h2h3. The h1 is the fruit name in the hero. Each content block opens with h2; the four prep steps use h3. The availability strip uses role="status" rather than a heading so it doesn't perturb the outline. Sidebar sections also use h2 so screen readers can navigate them.

Season parser

seasonStatus(raw: string, today = new Date()): SeasonStatus

Recognises:

  • "Year-round" / "Year-round (imported)"year-round (green strip)
  • "Mon – Mon" (en-dash or hyphen) → in-season if today is inside the window, shoulder for the calendar months immediately before/after, otherwise out-of-season
  • Anything else (e.g. "Limited drops") → unknown, neutral strip

Wrap-around windows like "Sep – Feb" are handled.

SEO

generateMetadata reads the seo block on the FruitRecord first, falling back to "How to eat {Name} | Preparation Guide — Fruit Plug" and the fruit's blurb when the taxonomy hasn't authored one. Canonical is /fruit/{slug}. The page emits two JSON-LD blocks: BreadcrumbList and Article (the fruit is the subject; image and inLanguage: en-GB set).

When fruit-wiki-facts.ts has entries for the slug the Article schema gains additionalProperty PropertyValue rows (one per fact, labelled Origin / Cultivars & relatives / Notable fact) and a citation array of Wikipedia article URLs.

Every prep guide is added to app/sitemap.ts at priority 0.7, beside the existing family routes.

app/p/[slug]/page.tsx looks the slug up against the taxonomy (getFruitBySlug) and, when there's a match, renders a "Preparation guide" card under the long description. The link uses as Route for typedRoutes.

Content rules (no fabrication)

The taxonomy's description is the source of truth. The page renders it verbatim, paragraph-split. The only generated copy is the four-step "How to eat" block, which is intentionally generic — ripeness cues, wash + chill, cut technique, simple pairings. Anything cultivar-specific must live in the taxonomy file, where it's reviewed once.

When a fruit has no description set, only the blurb + the generic prep block render. We do not invent a description.

Wikipedia enrichment — fruit-wiki-facts.ts

Each fact in apps/web/lib/fruit-wiki-facts.ts must be verifiable against the cited Wikipedia article as of the cache date. Claims that the source doesn't directly support are flagged partial in reports/wiki-fact-cache-2026-04-25.json and kept at species-level rather than invented. When re-running enrichment:

  1. Read the JSON cache first; only re-fetch slugs whose article changed materially.
  2. Keep the three-fact order fixed (origin → cultivars → quirky) so the additionalProperty JSON-LD labels line up.
  3. Never duplicate claims already present in the taxonomy's description — the point of this block is to add verified context, not restate copy.
  4. apps/web/lib/fruit-taxonomy.ts is read-only from the enrichment pass — new facts go in the sibling file.