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
&,,, orand: 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 malformedseason_ukvalue 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:
ItemListof fruits in peak this month, each with a stable/fruit/{slug}URL. - Sitemap:
/calendaris registered inapps/web/app/sitemap.tswithchangeFrequency: "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. |