zod で frontmatter を契約にする:記事の書式エラーをビルド時に落とす

Content Collections の schema が唯一の真実です。タイトルが 120 字を超える、日付が壊れている、タグが配列でない——どれもビルド時に失敗し、ページが黙って消えることはありません。

記事を書くときに最も多い失敗は内容ではなく書式です。引用符の欠落、日本語の日付、タグの角括弧忘れ。その結果、ページが静かに消えるか、奇妙な表示になります。

schema は一箇所だけ

src/content.config.ts が唯一の真実です。zod は astro:content ではなく astro/zod から読み込みます。

const blog = defineCollection({
  loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog', retainBody: true }),
  schema: z.object({
    title: z.string().max(120),
    description: z.string().max(300).default(''),
    date: z.coerce.date(),
    tags: z.array(z.string()).default([]),
    draft: z.boolean().default(false),
    featured: z.boolean().default(false),
  }),
});

それぞれが何を止めるか

120 字の制限は「要約をタイトルにしてしまう」ことを止めます。この値は title タグ、og:title、JSON-LD にも入り、検索結果では切り詰められます。

日付は z.date() ではなく coerce を使います。YAML では文字列だからです。これで 2026-08-13 は通り、日本語表記は拒否されます。

tags draft featured には既定値があるため省略できますが、書くなら配列と真偽値でなければなりません。引用符付きの draft: true は拒否されます。

retainBody と言語

retainBody: true は生の entry.body を保持し、読了時間の計算に使います。CJK は文字数、それ以外は空白区切りの語数として合算します。言語は entry.id の接頭辞から導出するため、記事を足してもルート設定は不要です。

schema が守れないこと

非 ASCII の slug、三言語のうち二言語しかない記事、「本記事では〜を紹介します」という説明文は防げません。これらは執筆ガイドが担当し、三言語そろえることを必須にしています。

変更の向き

コンテンツが schema に合わせるのであって、逆ではありません。制約を緩めるなら、まず schema を直し、次にガイドを更新し、最後に記事へ触ります。逆順にすると、手元では動いてビルドだけ壊れるコミットが生まれます。

ビルド時の失敗は 5 秒で済みます。ページが消えることは、消えたことに気づけないことで代償を払います。

← 記事一覧に戻る

コメント

…