Skip to content

Seasonal calendar — /calendar + home strip

A live, month-by-month UK seasonality view, driven entirely off the season_uk field already present on every FruitRecord in apps/web/lib/fruit-taxonomy.ts. Two surfaces:

Surface Purpose
Home page strip (SeasonalStrip) "What's peak right now" — six fruit cards with a CTA tail card linking to /calendar. Sits between the TrustStrip and the social-proof band.
/calendar page The full year. 12-column grid on desktop, vertical stack on mobile, today's column highlighted. Includes a JSON-LD ItemList of the current month's peak fruits.

Source of truth

The taxonomy editors write seasonality strings for humans, not machines. Examples taken from the catalogue:

  • "Year-round (imported)"
  • "Jun – Aug"
  • "Oct – Mar" (wraps the year boundary)
  • "Apr – Aug"
  • "Dec – Apr (flown in)"
  • "Limited drops" (rare — one fruit, the cacao pods)

A single forgiving parser lives in apps/web/lib/seasonality.ts.

Parser behaviour

parseSeason(str) returns { months, yearRound, peak?, shoulder? }.

  • Tolerates dash variants: , , , -, .
  • Accepts both abbreviated (Jun) and full (June) month names.
  • Recognises year-round phrasing: year-round, all year, imported year-round.
  • Wraps ranges across the year boundary, so "Oct – Mar" expands to [10, 11, 12, 1, 2, 3].
  • Parses compound expressions joined by &, ,, or and: e.g. "Oct–Dec & Apr–May"[10, 11, 12, 4, 5].
  • Parses explicit Peak: ... , Shoulder: ... labels — the only way the parser distinguishes peak from shoulder. Without those labels, every listed month is treated as peak.
  • Defaults to { yearRound: true } for anything it can't understand. Under-claiming availability is worse than over-claiming for a fruit catalogue, so a malformed season_uk value never throws and never hides the fruit.

The string "Limited drops" is currently treated as year-round by this default. That's a known under-classification — if seasonality of the cacao pods becomes commercially important, give them an explicit window.

Helpers

parseSeason(str)                        // raw string → ParsedSeason
fruitSeason(record)                     // FruitRecord → ParsedSeason
isInSeasonNow(record, ref?)             // 'peak' | 'shoulder' | 'off'
currentPeakFruits(all, ref?)            // FruitRecord[] sorted premium-first
currentShoulderFruits(all, ref?)        // FruitRecord[] sorted premium-first
fruitsByMonth(all)                      // 12-element array, peak/shoulder per month

ref defaults to a live new Date() so the home strip and /calendar re-render with the current month on every revalidation.

Visual design

  • Peak fruits are leaf-green (var(--fp-leaf)).
  • Shoulder fruits are amber (var(--fp-sticker)).
  • Out-of-season fruits are muted on the calendar grid and hidden from the home strip.
  • Today's column on the desktop calendar is highlighted with a tinted leaf-green background and a "Now" pill.

Screenshot description

The home page strip shows six square fruit cards in a horizontal row on desktop (single-row scroll on mobile). Each card has a transparent-PNG hero image, the fruit's display name and italic scientific name, plus a small leaf-green "Peak" pill in the top-left corner. The seventh card is a dashed CTA tail with the headline "See the full year" linking to /calendar.

The /calendar page opens with a hero block, a current-month summary band (with up to six peak fruits as small thumbnails), and the 12-column grid below. On desktop, every column is a card containing leaf-green peak names and amber shoulder names; today's column is the only one with a tinted background. On mobile, the same data renders as a vertical stack of month cards using pill-shaped tags instead of a list.

SEO

  • Canonical: /calendar.
  • Open Graph image: the local PNG of the first peak fruit (cycles with the season).
  • JSON-LD: ItemList of fruits in peak this month, each with a stable /fruit/{slug} URL.
  • Sitemap: /calendar is registered in apps/web/app/sitemap.ts with changeFrequency: "daily" because the page content rotates with the current month.

Files

Path Role
apps/web/lib/seasonality.ts Parser + helpers.
apps/web/components/seasonal/SeasonalStrip.tsx Home page strip.
apps/web/app/calendar/page.tsx /calendar route.
apps/web/components/atlas/AtlasPeek.tsx Family card "In season now" badge.
apps/web/app/page.tsx Inserts <SeasonalStrip /> between <TrustStrip /> and the stats band.
apps/web/app/sitemap.ts Adds /calendar to the static routes.