// Bilingual content schema for the Astro migration. // // Both `en` and `pt` are required on every localized field. A missing `pt` // must fail the build — silent English fallback is how a bilingual site // quietly becomes monolingual. Do not change `localized` to make a field // optional without an explicit reason in a code review. // // Layout follows the brief in plans/astro-refactor/task-04-content-schema.md: // - phases / providers / efforts / skillSources / handsOnPrompts / // skillInstallPrompts — guide content carried over from app.js // - chapters — copy for /models/, /agents/, /skills/, /summary/ // - reviews — the 24 entries from skills-review/catalog.js // // This file is schema only. Tasks 05 and 06 fill the entries; the content // migrator agent owns this file end-to-end. import { defineCollection, z } from 'astro:content'; import { glob } from 'astro/loaders'; // One localized field. `en` and `pt` are mandatory strings; no nullish, // no default. A missing `pt` is a build error, not a silent fallback. const localized = z.object({ en: z.string(), pt: z.string() }); // Phases from `phases` in app.js: plan / build / review. Each phase has a // model recommendation, title, prose, and a code-flavoured summary line. const phases = defineCollection({ type: 'data', schema: z.object({ id: z.string(), model: localized, title: localized, copy: localized, code: localized, }), }); // Providers from `modelGuide.providers` in app.js: openai / claude / gemini. // Tiers are a 3-tuple of [kind label, name, note-localized]. const providers = defineCollection({ type: 'data', schema: z.object({ id: z.string(), label: z.string(), source: z.string().url(), title: localized, copy: localized, tiers: z.array(z.tuple([z.string(), z.string(), localized])), config: z.string(), }), }); // Efforts from `modelGuide.efforts` in app.js: low / medium / high. Each // effort carries three lines per locale (heading, body, code-line). const efforts = defineCollection({ type: 'data', schema: z.object({ id: z.string(), en: z.array(z.string()), pt: z.array(z.string()), }), }); // Skill source URLs from `skillSources` in app.js: pinned commit URLs for // each public skill this site references. const skillSources = defineCollection({ type: 'data', schema: z.object({ id: z.string(), url: z.string().url(), }), }); // Hands-on prompts from `handsOnPrompts` in app.js: per-locale basic and // skills variants for the Tiny Tasks exercise. const handsOnPrompts = defineCollection({ type: 'data', schema: z.object({ id: z.string(), en: z.object({ basic: z.string(), skills: z.string() }), pt: z.object({ basic: z.string(), skills: z.string() }), }), }); // Skill install prompts from `skillInstallPrompts` in app.js: per-locale // install instructions that name the pinned commits and the safety rules. const skillInstallPrompts = defineCollection({ type: 'data', schema: z.object({ id: z.string(), en: z.string(), pt: z.string(), }), }); // Chapter copy for /models/, /agents/, /skills/, /summary/ and the landing // route map. The body of each page composes one or more sections; the // field names mirror what the existing HTML already says aloud (eyebrow / // lede / card titles). // // `cards[].href` and `cards[].cta` were added in task 04b: the landing // route map's six cards have non-uniform call-to-action text (four read // "Open chapter →", one "Open lab →", one "Open desk →"), so a single // hard-coded default would be wrong. `href` is the destination; `cta` is // the localized link text. // // `sections[].panelLabel` / `panelCode` / `panelHint` carry the highlighted // rule panels on /models/, /agents/, and /skills/ (the routing rule, the // MAIN / ORCHESTRATOR tree, and the package-anatomy hint). // // `threadLabel` / `threadText` and `footer` carry the landing-page thread // strip and footer line. const chapters = defineCollection({ type: 'data', schema: z.object({ id: z.string(), eyebrow: localized, title: localized, lede: localized, threadLabel: localized.optional(), threadText: localized.optional(), footer: localized.optional(), cards: z .array( z.object({ label: localized, title: localized, copy: localized, href: z.string().optional(), cta: localized.optional(), }), ) .optional(), sections: z .array( z.object({ eyebrow: localized.optional(), title: localized, copy: localized.optional(), panelLabel: localized.optional(), panelCode: localized.optional(), panelHint: localized.optional(), steps: z .array( z.object({ label: localized, copy: localized, }), ) .optional(), }), ) .optional(), }), }); // Workers from `interactiveCopy.workers` in app.js: the three sub-agent // cards on /full-guide/. Each entry holds a 3-element array per locale // (`[label, detail, result]`), not a localized object — the renderer // unwraps it directly: `worker[language()][0..2]`. const workers = defineCollection({ type: 'data', schema: z.object({ id: z.string(), en: z.array(z.string()), pt: z.array(z.string()), }), }); // Trees from `interactiveCopy.trees` in app.js: the four worktree nodes // (main / ui / tests / docs). `owner` and `note` are localized; `path` // and `command` are single shell strings (the git command shown to the // user). `status` is rendered by index.html, not the GuideSelector island, // but it ships in the data and the schema keeps it so nothing is dropped. const trees = defineCollection({ type: 'data', schema: z.object({ id: z.string(), status: z.string(), owner: localized, path: z.string(), command: z.string(), note: localized, }), }); // Routes from `interactiveCopy.routes` in app.js: plan / build / explore / // review. `score` is the numeric reasoning-load value (rendered into // `style="--score:N%"`), `label` and `why` are localized. const routes = defineCollection({ type: 'data', schema: z.object({ id: z.string(), score: z.number(), label: localized, why: localized, }), }); // Skill files from `interactiveCopy.skillFiles` in app.js: SKILL.md / // references/ / scripts/ / assets/. `icon` and `title` are unlocalized // labels; `en` and `pt` are separate top-level strings, not a localized // object — matching how `renderSkillFile` reads them. const skillFiles = defineCollection({ type: 'data', schema: z.object({ id: z.string(), icon: z.string(), title: z.string(), en: z.string(), pt: z.string(), }), }); // Skill workflow from `interactiveCopy.skillWorkflow` in app.js: the five // steps of building a skill (observe / trigger / scaffold / write / // validate). `number` is unlocalized; `title` / `question` / `action` / // `output` / `proof` are all localized. const skillWorkflow = defineCollection({ type: 'data', schema: z.object({ id: z.string(), number: z.string(), title: localized, question: localized, action: localized, output: localized, proof: localized, }), }); // Common skills from `interactiveCopy.commonSkills` in app.js: the seven // shared skill cards on /full-guide/. `number` and `title` are unlocalized; // `kind` / `rule` / `use` / `example` / `caution` are localized. `source` // is the GitHub URL — in app.js it is read from `skillSources[id]` at // render time; here it is embedded to match the GuideSelectorData // contract. If the URL changes, both this entry and the matching // `skillSources/.json` must be updated. // // `label` and `tagline` are the selector *button*'s two lines and are // distinct from `kind` and `use`, which belong to the detail panel: // ponytail's button reads "SIMPLIFY" / "minimum code that holds" while its // panel reads "SIMPLIFICATION INSTINCT" and a paragraph of prose. They are // not in `interactiveCopy` — the English lives in the `full-guide/index.html` // markup and the Portuguese in `translations.pt` — so task 05b had nothing // to migrate them from and the page rendered `kind`/`use` on the buttons. const commonSkills = defineCollection({ type: 'data', schema: z.object({ id: z.string(), number: z.string(), kind: localized, label: localized, tagline: localized, title: z.string(), rule: localized, use: localized, example: localized, caution: localized, source: z.string().url(), }), }); // Review desk entries from skills-review/catalog.js + // skills-review/submitted-catalog.js: 24 in total. Each entry is a Markdown // file with frontmatter for the metadata and a body that holds the // `improved` SKILL.md content. Astro renders the body via `` and // exposes the raw markdown via `entry.body` for the review desk's diff view. // // `name` is preserved separately from `id`: two entries renamed the skill // during review (id `angular-accessibility-root` → name // `angular-accessibility`, id `confectionary-skill-hub` → name // `confectionery-orders`). The build script uses `name` to keep // `skill-reviews/improved/**/SKILL.md` byte-identical. const reviews = defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/reviews' }), schema: z.object({ id: z.string(), author: z.string(), path: z.string(), title: z.string(), status: z.string(), focus: z.string(), wins: z.array(z.string()), improve: z.array(z.string()), extras: z.string(), name: z.string(), description: z.string(), }), }); export const collections = { phases, providers, efforts, skillSources, handsOnPrompts, skillInstallPrompts, chapters, workers, trees, routes, skillFiles, skillWorkflow, commonSkills, reviews, };