overhaul: 04 content model - #18
Conversation
|
CS-5
left a comment
There was a problem hiding this comment.
Code review of the content model layer. Verified the migration against legacy first: sponsors are lossless (all 7 active + the 3 real commented entries, levels and supportSince values all match legacy/data/sponsors.ts), the 6 FAQ answers are verbatim, both calendar IDs match legacy/src/app/calendar/[name]/page.tsx byte for byte, team photos are the full 13 FRC + 1 FLL set with captions intact, and every one of the 33 relative image paths in frontmatter resolves. pnpm check && pnpm build are green as claimed, and the level: Titanium failure reproduces exactly as described. Skipping "Test Sponsor" was the right call.
Eleven findings, two of which I'd want resolved before this merges:
sharpis unresolvable, so the first page to render a collection image fails the build (src/content.config.ts) — reproduced with a throwaway page:pnpm buildexits 1 withMissingSharp. This PR is what makes it load-bearing, moving 33 images frompublic/intosrc/assets/behindimage(). Invisible today only because no page consumes a collection yet.- A typo'd frontmatter field name is silently dropped (
src/content.config.ts) —sinceYear: 2013builds clean and exits 0.docs/content.mdtwice promises the opposite, and the acceptance criterion was verified with a bad value rather than a bad field name..strict()closes it.
Then: the programs map is missing sc2 while the enum accepts it (and openhouse.md uses it); z from astro:content is deprecated for removal in the major already pinned; the canonical origin is now duplicated with astro.config.ts; a comment points at a functions/api/calendar/ that doesn't exist; a dead docs link; four kickoff links and two robot logos dropped without a note; Robo Fett's "2020-2021" label lost to the single year; an empty h2 in the kickoff body; one orphaned event hero.
The disclosed dangling-content-reference gap I'd leave as-is — flagging it and deferring the throw to Phase 08 is the right shape, and it's documented as a sharp edge.
The three deviations from the brief are all well-argued; the slashed-collection-name finding in particular is a good catch worth the deviation.
Generated by Claude Code
04662c7 to
43c4be8
Compare
43c4be8 to
ca44bb8
Compare
ca44bb8 to
c5744f1
Compare
Everything that changes over time moves out of code and into zod-validated markdown. Adding a sponsor is now a logo file plus five lines of frontmatter. Migrated: 10 sponsors (7 active, 3 as active: false), 6 FAQ answers, both seasonal events, 6 robots, 14 team photos. Copy comes across verbatim, typos included — revision is Phase 12 (D8). Former sponsors are records with active: false rather than commented-out code, so we can still say who supported us in 2022. src/data/site.ts replaces every hardcoded constant the legacy pages carried: the four external URLs from data/config.ts, both Google Calendar IDs, the GA4 measurement ID, the workspace address and directions link, socials and contact. Schemas stay flat on purpose (D2) — strings, enums, booleans, dates, numbers, images — which is what keeps a git-backed CMS a later addition rather than a restructuring. events therefore carries locationName/locationAddress instead of a nested object. Two things worth knowing: Collection names avoid slashes (frcRobots, not frc/robots) while the content still nests under frc/ and fll/ on disk as D18 requires. Astro writes each collection's editor JSON schema to .astro/collections/<name>.schema.json without creating intermediate directories, so a slashed name warned on every build and silently dropped frontmatter autocomplete. news/template.md is a real entry with draft: true rather than a glob-excluded _TEMPLATE.md. An excluded template drifts from the schema unnoticed and leaves the collection empty, which warns on every build; as an entry it is validated and still never renders. "Test Sponsor" was deliberately not migrated — it is the one commented-out legacy entry that is a test fixture, not a former sponsor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
`image()` in the schemas commits every moved asset to astro:assets, but pnpm's isolated layout keeps Astro's own `sharp` where the bundled image service cannot resolve it — the first page to render a collection image gets one `MissingSharp` warning per asset and an unoptimized passthrough. Reproduced, then declared `sharp` in dependencies with ADR 0003. `z.object` strips unknown keys, so a typo'd field name built clean and silently dropped the value — the one thing `docs/content.md` promises it does not do. The acceptance criterion had been verified with a bad *value*, which always failed correctly. All seven schemas are `z.strictObject`; `sinceYear` for `since` now exits 1 with `Unrecognized key`. The `program` enum accepted `sc2` (the open house uses it) while `programs` had only `frc` and `fll`, so `programs[entry.data.program]` was undefined for that event. The enum derives from `PROGRAM_KEYS` and the map `satisfies` a total record over it. `astro.config.ts` reads `site.url` rather than declaring the canonical origin a second time. `z` from `astro:content` is deprecated with removal slated for the major already pinned — switched to `astro/zod`, and `z.string().url()` to Zod 4's `z.url()`, clearing 53 check hints. Content that legacy had and no schema field could hold: the kickoff's two season teaser URLs and two game-hint links (`teaserUrls`/`hintUrls`/`hintLabels`, flat per D2), its hero subtitle, which had become an `##` with no section under it, Robo Fett's "2020-2021" two-season label, and the Viper and TroubleClef wordmark overlays. The kickoff `description` is legacy's own again rather than new prose. Reference integrity is now enforced where the reference is declared: `tools/checks/content-references.mjs` fails `pnpm check` on a dangling `faq` slug, which Astro only logs before exiting 0. Quality: the workspace address and directions URL were written into every event's frontmatter next to the copies in `site.ts`; location is optional and defaults from there, so only an off-site event carries one. `displayDate` restated `start`/`end` as prose, and the open house stated its date twice more — in body copy and in an FAQ answer; `src/lib/event-date.ts` formats the timestamps, and Phase 08's acceptance criteria require closing its knip seam. Dropping the blanket `src/data/site.ts` knip entry restored export-level checking there, with `@public` on the two exports that have a known future consumer. The moved masters were camera-resolution — up to 6000px and near-lossless — so `<Image>` derived variants from them that came out larger than the source and cost ~22s of sharp time per build. `tools/assets/optimize-sources.mjs` (plan/09 §2) caps sources at 2560px: 17.8 MB saved across 12 files. `openhouse-header.webp` went back to `public/image`, where Phase 09's inventory-based prune can still see it — nothing references it. Also: `src/data/site.ts` described a `functions/api/calendar/` that does not exist, and `docs/content.md` linked to a route that does not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
c5744f1 to
1963831
Compare
Layer 4 of the overhaul stack, on
overhaul/03-primitives.plan/04-content-model.md.Content moves out of code into zod-validated markdown. This is the objective-2 layer: adding a sponsor should take two minutes and one small PR — it now takes a logo file and five lines of frontmatter.
Migrated
sponsorslegacy/data/sponsors.tsfaqevents/openhouseand/programs/frc/kickofffrcRobotsfrcTeamPhotos/fllTeamPhotosabout/components.tsxnewsSponsor migration is lossless. Active: JLG (Platinum), The WorkShope, Y.B. Welding, Journalytic, Volvo (Gold), Orrstown, Manitowoc (Bronze). Retired as
active: false: Wellspan, VFW, Fives — all three were commented-out blocks in the legacy file, so we can still say who supported us in 2022 instead of losing it to a code comment.One entry I deliberately did not migrate:
Test Sponsor. It's the fourth commented-out block, but it's a test fixture — no logo,example.comURL. Migrating it would have invented a sponsor. Flagging in case you'd rather it exist as a fixture.Copy comes across verbatim, typos included ("beings", "atriculate", "visibilty") — revision is Phase 12 per D8.
src/data/site.tsreplaces every hardcoded constant the legacy pages carried: the four URLs fromdata/config.ts, both Google Calendar IDs, the GA4 measurement ID, the workspace address and directions link, socials, contact email, plus aprogramsmap keyed by theme name.docs/content.mdis the editing guide — add and retire a sponsor, add an FAQ, update or hide an event, add a robot and a team photo, each with a copy-paste template, plus a "sharp edges" section.Verified
Validation genuinely bites. Setting a sponsor's level to
Titanium:…and
pnpm buildexits 1.pnpm check && pnpm buildgreen.Three things I changed from the brief, each for a reason
1. Collection names avoid slashes —
frcRobots, notfrc/robots. The content still nests underfrc/andfll/on disk exactly as D18 requires; only the collection key changed. Astro writes each collection's editor JSON schema to.astro/collections/<name>.schema.jsonwithout creating intermediate directories, so a slashed name warned on every build and silently lost frontmatter autocomplete for those three collections.2.
news/template.mdis a real entry withdraft: true, not a glob-excluded_TEMPLATE.md. An excluded template drifts from the schema unnoticed, and it leaves the collection empty — which warns on every build. As an entry it's schema-validated and still never renders.3.
eventscarries flatlocationName/locationAddressinstead of the brief's nestedlocation: { name, address }, per D2's flat-schema rule. Also addeddisplayDate(legacy's kickoff page kept a hand-written date string next to the timestamp, and that reads better in copy than anything we'd format) anddirectionsUrl.One gap worth knowing about
A mistyped content reference logs an error but exits 0. Astro reports:
…and then builds successfully. So a typo'd FAQ slug in an event would silently drop that answer from the page. It's documented as a sharp edge in
docs/content.md, and Phase 08 — which actually consumes these references — should throw on a missing one rather than rendering a short list.Assets
Moved only what the collections reference: sponsor logos, team photos, the five robot photos, two event heroes.
public/imageis down from 36 MB to 13 MB as a side effect. The full inventory and prune is Phase 09's job.Also carried the Phase 04 guardrail into
AGENTS.mdas review guidance rather than a lint rule (the brief allowed either): never inline a content array where a collection exists, and new repeating content earns a collection. A lint rule that recognizes "an array that should be a collection" would be guesswork.Generated by Claude Code