Files
ai-for-dummies/plans/astro-refactor/task-04-content-schema.md
T
Marcos Paulo b554f87d51 feat(content): define bilingual content collection schema
Add src/content/config.ts with strict { en, pt } schema and eight
typed collections matching the shapes in app.js and
skills-review/catalog.js: phases, providers, efforts, skillSources,
handsOnPrompts, skillInstallPrompts, chapters, reviews.

Both locales are required on every localized field. A deliberately
missing pt fails the build with InvalidContentEntryDataError, proved
with a probe entry and reverted. Silent English fallback is what turns
a bilingual site monolingual; the schema must not allow it.

Did not move any content yet. Tasks 05 and 06 fill the entries
against the shape defined here, in parallel.

Also records the language-switching decision in the task brief:
client-side swap, both locales in the payload, html lang tracks the
active language. Behaviour parity, schema fit, and tiny payload
size beat the SEO upside of route-based i18n for this site.

astro check: 0 errors, 0 warnings. npm run verify: green. No
assertion count change.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-05 02:58:14 +00:00

3.5 KiB

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:
    • guidephases, 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 <html lang> 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 <html lang> 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.

<html lang> must still track the active language regardless of how the strings reach the page.

Done when

  • 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)
  • 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.
  • 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, <html lang> 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.