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 秒で済みます。ページが消えることは、消えたことに気づけないことで代償を払います。

コメント
…