Astro Content Collections と Zod で frontmatter を型安全にする

フロントエンド

Markdown でブログを書いていると、frontmatter の書き間違いが地味に効いてくる。publishDatepubDate と書いてしまったり、日付のフォーマットがバラついたり。Astro の Content Collections を使えば、これを Zod スキーマで型として縛れる。

スキーマを定義する

Astro 5 以降、コレクションの設定は src/content.config.ts に置く。glob() ローダーで Markdown を読み込み、schema に Zod を書く。

ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ base: './src/content/blog', pattern: '**/*.md' }),
  schema: ({ image }) =>
    z.object({
      title: z.string(),
      description: z.string(),
      publishDate: z.coerce.date(),
      updatedDate: z.coerce.date().optional(),
      tags: z.array(z.string()).default([]),
      draft: z.boolean().default(false),
      heroImage: z.optional(image()),
    }),
});

export const collections = { blog };

ポイントをいくつか。

  • z.coerce.date() にしておくと、'2026-06-29' のような文字列を自動で Date に変換してくれる。
  • tags.default([]) で省略可能にしている。
  • draft を持たせておくと、下書きを一覧や RSS から除外できる。

下書きを隠す

draft: true の記事を出し分けるには、getCollection の第2引数でフィルタするだけでよい。

ts
const posts = await getCollection('blog', ({ data }) => !data.draft);

一覧ページ・個別ページ・RSS のすべてで同じフィルタをかけておけば、公開判断は frontmatter の draft フラグを切り替えるだけになる。

型が効く安心感

スキーマを定義しておくと、post.data.publishDateDate 型として扱えるし、存在しないフィールドを参照すればエディタが警告してくれる。「仕様書を書く感覚でコンテンツ構造を設計できる」のがこの仕組みのいいところだ。

ゆべしま
フロントエンドエンジニア。型と開発体験の話が好き。週末は個人開発とコーヒー。