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>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// 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';
|
||||
|
||||
// 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/. 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).
|
||||
const chapters = defineCollection({
|
||||
type: 'data',
|
||||
schema: z.object({
|
||||
id: z.string(),
|
||||
eyebrow: localized,
|
||||
title: localized,
|
||||
lede: localized,
|
||||
cards: z
|
||||
.array(
|
||||
z.object({
|
||||
label: localized,
|
||||
title: localized,
|
||||
copy: localized,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
sections: z
|
||||
.array(
|
||||
z.object({
|
||||
eyebrow: localized.optional(),
|
||||
title: localized,
|
||||
copy: localized.optional(),
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
label: localized,
|
||||
copy: localized,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Review desk entries from skills-review/catalog.js +
|
||||
// skills-review/submitted-catalog.js: 24 in total. `improved` is Markdown
|
||||
// source text, kept as a string here so the diff view that compares
|
||||
// original-vs-improved still has the raw text available. When the
|
||||
// markdown render path lands, this can become a body field without
|
||||
// changing the entry shape.
|
||||
const reviews = defineCollection({
|
||||
type: 'data',
|
||||
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(),
|
||||
improved: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
phases,
|
||||
providers,
|
||||
efforts,
|
||||
skillSources,
|
||||
handsOnPrompts,
|
||||
skillInstallPrompts,
|
||||
chapters,
|
||||
reviews,
|
||||
};
|
||||
Reference in New Issue
Block a user