💡 Tips

Astro Content CollectionsのZodスキーマ設計|個人開発ブログを型安全に構築する実装手順

結論:Zodスキーマは「最初に厳しく作って後で緩める」が正解

Astroでブログを個人開発するなら、Content CollectionsのZodスキーマは最初から必須フィールドを厳しめに定義するべきだ。

理由は単純。

  • 緩いスキーマは後からフィールドを増やすたびにMarkdownを全部書き直す羽目になる
  • ビルド時にエラーが出ないと、本番デプロイ後に「日付が表示されない」といった不具合で気づく

以下、実際の設計パターンと手順を順に見ていく。

Content Collectionsとは何か(前提の整理)

Astro Content Collectionsは、src/content/配下のMarkdown/MDXファイルを「型付きのデータ」として扱う仕組みだ。

src/content.config.ts(Astro 4系以降はsrc/content/config.tsではなくsrc/content.config.tsが推奨)にZodスキーマを書くと、次のメリットがある。

  • フロントマターの型が自動生成される
  • ビルド時にスキーマ違反を検出できる
  • getCollection()で取得したデータにIDEの補完が効く

Astroのバージョンによって設定ファイルの置き場所やAPIが変わることがあるため、着手前に公式ドキュメントの最新情報を確認してほしい。

実装手順

手順1: コレクションを定義する

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

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

export const collections = { blog };

ポイントは3つ。

  • z.coerce.date()を使うと、フロントマターの日付が文字列でもDate型に変換される
  • .default([])で配列フィールドの記述漏れを防ぐ
  • titleとdescriptionは文字数制限をかけてSEO事故を防ぐ

手順2: 画像フィールドを型安全にする

アイキャッチ画像はパスの文字列ではなくimage()ヘルパーを使うと、存在しないファイルをビルド時に検出できる。

schema: ({ image }) => z.object({
  title: z.string(),
  heroImage: image().optional(),
  heroImageAlt: z.string().optional(),
})

heroImageを指定したのにheroImageAltを書き忘れる、というアクセシビリティ上の抜けもZodの.refine()で相互依存チェックが可能だ。

.refine(
  (data) => !data.heroImage || !!data.heroImageAlt,
  { message: 'heroImageを指定する場合はheroImageAltも必須です' }
)

手順3: カテゴリを列挙型で固定する

自由入力のcategory: z.string()は、typoで「Tips」「tips」「TIPS」が混在する温床になる。個人開発ブログではz.enum()で選択肢を固定するのが安全だ。

category: z.enum(['tech', 'tips', 'diary', 'review']),

新しいカテゴリを増やすときはスキーマの修正が必須になるが、これは「意図しないカテゴリの乱立を防ぐ」という設計上のトレードオフとして受け入れるべきだ。

手順4: 取得側での型活用

---
import { getCollection } from 'astro:content';
const posts = await getCollection('blog', ({ data }) => !data.draft);
---

data.draftやdata.pubDateにはスキーマで定義した型がそのまま反映されるため、any型に頼らずビルド全体の型安全性が保てる。

スキーマ設計パターンの比較

パターンメリットデメリット向いているケース
全フィールドrequired記述漏れをビルド時に確実に検出記事追加の初速が落ちる記事数が少ない立ち上げ期
主要フィールドのみrequired+.default()執筆のハードルが低い意図せぬデフォルト値が紛れ込む継続的に量産するフェーズ
z.discriminatedUnionで記事種別ごとにスキーマ分岐レビュー記事・お知らせ等で必須項目を出し分けられるスキーマの見通しが複雑になる記事タイプが3種類以上ある

個人開発で最初に迷うなら、記事数が少ないうちは「全フィールドrequired」で始め、量産期に入ったら緩めるのが移行コストが低い。

詰まりやすいポイント

  • 日付のタイムゾーン: z.coerce.date()はUTC扱いになるため、JSTでの表示がずれることがある。表示側でIntl.DateTimeFormatのtimeZone指定を忘れないこと
  • Markdown本文中の画像とheroImageの二重管理: image()ヘルパーはフロントマター専用。本文中の画像は別途最適化の仕組みが必要
  • スキーマ変更時の既存記事: 必須フィールドを追加すると既存のMarkdownが全部ビルドエラーになる。移行スクリプトか.optional()からの段階移行を検討する

このあたりの落とし穴は、AstroでやるべきSEO対策まとめ|静的サイトで検索流入を伸ばす設定でも触れているメタデータ設計と地続きなので、あわせて確認しておくと手戻りが少ない。

デプロイ先の検討

Astroの静的サイトはVPSや共有サーバーでも配信できるが、ビルドパイプラインとの相性を考えるとCloudflare Pagesのような静的ホスティングが手間が少ない。すでにCloudflare PagesにAstroサイトをデプロイする方法|手順と詰まりどころで手順を解説しているので、デプロイ設定に迷ったら参照してほしい。

個人でサーバーごと管理したい場合は

のようなVPSも選択肢になるが、静的サイトのみなら過剰スペックになりやすい点は正直に書いておく。

まとめ

Astro Content CollectionsのZodスキーマ設計は、次の順で進めると失敗が少ない。

  1. 必須フィールドを厳しめに定義してビルド時エラーを最大限活用する
  2. 画像はimage()ヘルパーで型安全に扱う
  3. カテゴリなど選択肢が限られるフィールドはz.enum()で固定する
  4. 記事量産フェーズに入ったら.default()や.optional()で執筆コストを下げる

Astroのバージョンアップで設定ファイルの仕様が変わることがあるため、実装前には必ず公式ドキュメントの最新情報を確認してほしい。