# Task 04 — Content collection schema **Agent**: `content-i18n-migrator` · **Model**: MiniMax-M3 **Depends on**: 01 · **Parallel with**: 02, 03 · **Blocks**: 05, 06 **Worktree**: `.agents/scripts/worktree.sh start 04 content-schema` ## Goal Typed collections that make a missing translation a build error. ## Scope `src/content/config.ts` only. You own it. ## Steps 1. Define `const localized = z.object({ en: z.string(), pt: z.string() })`. **Both required.** A missing `pt` must fail the build — silent English fallback is how bilingual sites quietly become monolingual. 2. Collections: - `guide` — `phases`, `modelGuide`, `skillSources`, `handsOnPrompts`, `skillInstallPrompts` from `app.js` - `chapters` — copy for `/models/`, `/agents/`, `/skills/`, `/summary/` - `reviews` — the 24 entries: `id`, `author`, `title`, `status`, `focus`, `wins[]`, `improve[]`, `extras`, `improved` 3. **Make the language-switching decision** and record it here: - _client-side swap_ — matches today, no URL change, both languages in the payload. **Recommended.** - _route-based `/en/` `/pt/`_ — better SEO, changes every existing URL, needs redirects. **Decision (2026-09-05, task 04):** **client-side swap.** Both locales ship in the payload, the existing toggle swaps text and `` in place, and nothing about today's URLs changes. Reasons: - **Behaviour parity.** The site today is a client-side swap. Choosing anything else means changing every URL that anyone has shared, plus adding redirects for `/full-guide/`, `/skills-review/`, and every chapter page. The cost is paid once at migration and the benefit is invisible to existing visitors. - **Content size.** ~50 bilingual keys in `app.js` plus 24 review entries. Shipping both locales in the payload is a few KB on top of what already loads — negligible compared to the JS the site ships today. - **Schema fit.** The `{ en, pt }` shape the existing code already uses maps one-to-one onto the `localized = z.object({ en, pt })` schema in this task. Route-based i18n would require a different schema (entries per locale) and force every consumer to pick a locale at the call site. - **SEO is a known trade-off, not a bug.** Search engines see the active language in `` and the toggle is reachable on every page. Crawlers that index only one language will index the rendered one — same as today. If a future task decides the SEO trade-off is no longer acceptable, the schema change is local: split each entry by locale, switch the collection loader, and add redirects. The decision is reversible. `` must still track the active language regardless of how the strings reach the page. ## Done when - [x] `astro check` passes — `Result (22 files): 0 errors, 0 warnings, 2 hints` (the two hints are pre-existing `document.execCommand` deprecations in vanilla JS, not from this schema) - [x] A deliberately missing `pt` field fails the build — proved with `src/content/phases/probe.json` (omitted `copy.pt`). Astro raised `InvalidContentEntryDataError: phases → probe data does not match collection schema. copy.pt: Required`. Reverted. - [x] The language decision is written down here with its reason — see "Decision (2026-09-05, task 04)" above: **client-side swap**, both locales in the payload, `` tracks active language. ## Do not Do not move any content yet. Schema only — tasks 05 and 06 fill it, and they run in parallel against the shape you define.