Astro Content Collections完全ガイド:概念からSchema検証の実践まで

はじめに
正直なところ、私がAstroでブログを書き始めた当初は、Content Collectionsの重要性を全く感じていませんでした。「Markdownファイルを src/pages/blog/ に置くだけでいいじゃん? ページが表示されればそれでOKでしょ?」そう思っていたのです。
ある日、ブログのトップページが真っ白になりました。エラーログには「publishDateの形式が不正です」と出ています。原因を探すため、30本の記事を1つずつ見直す羽目に……。そして30分後、ようやく見つけました。ある記事のfrontmatterで日付が 2024-12-01 ではなく 2024/12/01 と書かれていたのです。
たった30記事でこれです。もしこれが100記事だったら? 新しいフィールドを追加するたびに全ファイルをチェックするの? 考えただけで寒気がしました。
その日、私はContent Collectionsが単なる「カッコいい機能」ではなく、「現場の痛みを解決するツール」だと理解しました。これはAstro版のTypeScriptのようなものです。間違いを事前に検出し、Schemaを設定すればエディタが補完してくれる——もう「あのフィールド名、なんだっけ?」とドキュメントを漁る必要はありません。
この記事では、Content Collectionsとは何か、どう設定するのか、Schema検証をどう活用するのかを、私の失敗談を交えて分かりやすく解説します。
Content Collectionsとは? なぜ必要なのか?
「Markdownをフォルダ管理するのと何が違うの?」と思うかもしれません。確かに、機能的には src/pages/ 配下で管理してもブログは作れます。
しかし、最大の問題は**「型安全性がゼロ」**だということです。
従来の方法では、frontmatterは無法地帯です:
---
title: "私のブログ"
date: "2024-12-01"
tags: ["Astro", "チュートリアル"]
---
本文...一見問題なさそうですが、以下のシナリオを想像してください:
tagsをうっかりtag(単数形)と書いてしまった- 日付を
12/01/2024と書いてしまった - 新しく
authorフィールドを追加したが、過去記事への追記を忘れた
Astroはこれらのミスを教えてくれません。実行時にページが壊れて初めて気づくのです。
Content Collectionsはこれを解決します。
これは「型安全なコンテンツ管理システム」です。MarkdownファイルにTypeScriptの型チェックを導入すると考えてください。
具体的には以下の機能を提供します:
- Schema検証:frontmatterの構造と型を定義し、違反があれば即座にエラーを出す。
- 自動型生成:Schemaに基づいてTypeScript型を自動生成。エディタ補完が効く。
- 統一クエリAPI:
getCollection()などで安全にデータを取得。 - パフォーマンス:Astro 5.0のContent Layer APIによりクエリが高速化。
要するに、従来の方法が「自由だが危険」なら、Content Collectionsは「規律ある安心」です。一度設定すれば、99%の凡ミスを防げます。
私は現在、すべてのAstroプロジェクトでこれを採用しています。
Content Collections 設定実践
理論はこれくらいにして、実際に設定してみましょう。手順は「ディレクトリ作成」「設定ファイル作成」「コンテンツ作成」の3ステップです。
ステップ1:ディレクトリ作成
コンテンツは src/content/ ディレクトリに置くルールになっています(v2.0以降の予約ディレクトリ)。
src/
├── content/
│ ├── blog/ # ブログコレクション
│ │ ├── post-1.md
│ │ └── post-2.md
│ └── docs/ # ドキュメントコレクション
│ ├── guide-1.md
│ └── guide-2.md
├── content.config.ts # 設定ファイル(場所に注意!)
└── pages/
└── ...注意:設定ファイルは src/content.config.ts(または .js, .mjs)です。content/ ディレクトリの中ではありません! 私は最初これを間違えてハマりました。
各サブディレクトリが「コレクション」になります。src/content/blog/ は blog コレクションです。
ステップ2:設定ファイル作成
src/content.config.ts を作成します。ここが核心です。
// src/content.config.ts
import { defineCollection, z } from 'astro:content';
// blogコレクションの定義
const blogCollection = defineCollection({
type: 'content', // type: 'content' はMarkdown/MDXファイルを意味する
schema: z.object({
title: z.string(), // タイトル(必須)
description: z.string(), // 説明(必須)
pubDate: z.coerce.date(), // 公開日(文字列をDate型に自動変換)
tags: z.array(z.string()).optional(), // タグ配列(任意)
draft: z.boolean().default(false), // 下書きフラグ(デフォルトfalse)
}),
});
// collectionsオブジェクトをエクスポート
export const collections = {
'blog': blogCollection, // キー名はディレクトリ名と一致させる
};コードを分解して解説します:
defineCollection():コレクションの設定を定義します。type: 'content':Markdown/MDXファイルであることを指定します。schema:Zod(バリデーションライブラリ)を使ってfrontmatterの構造を定義します。collectionsエクスポート:定義したコレクションをエクスポートします。キー名はディレクトリ名と一致させてください。
schema 部分が重要です。Zodを使って型を定義します:
z.string():文字列z.coerce.date():文字列をDateオブジェクトに強制変換(超便利!)z.array(z.string()):文字列の配列.optional():省略可能.default(false):デフォルト値設定
ステップ3:コンテンツ作成
設定ができたら、src/content/blog/ にMarkdownファイルを作ります。
---
title: "Astro Content Collections入門"
description: "設定方法と使い方を学ぶ"
pubDate: "2024-12-01"
tags: ["Astro", "Tutorial"]
---
本文...もしfrontmatterがSchemaに違反していると(例:pubDate のフォーマットがおかしい)、Astroはビルド時にエラーを出して止めてくれます。
ページでのデータ取得
設定完了後、Astroファイル内で安全にデータを取得できます。
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
// ブログ記事を全件取得
const allPosts = await getCollection('blog');
// 下書きを除外
const publishedPosts = allPosts.filter(post => !post.data.draft);
---
<ul>
{publishedPosts.map(post => (
<li>
<a href={`/blog/${post.slug}`}>
{post.data.title}
</a>
<p>{post.data.description}</p>
</li>
))}
</ul>ここで注目してほしいのが、post.data(frontmatterデータ)に完全な型定義がついていることです。VS Codeで post.data. と入力すると、title、description、pubDate が候補に出てきます。これが最高に気持ちいい!
Schema検証:もっと使いこなす
z.string() などの基本だけではありません。Zodを使った高度な検証テクニックを紹介します。
基本タイプ速見表
import { z } from 'astro:content';
z.string() // 文字列
z.number() // 数値
z.boolean() // 真偽値
z.date() // Dateオブジェクト
z.coerce.date() // 文字列などをDateに変換
z.array(z.string()) // 文字列配列
z.enum(['draft', 'published']) // 列挙型(特定の値のみ許可)特筆すべきは z.coerce.date() です。YAMLのfrontmatterでは日付はただの文字列("2024-12-01")として扱われます。z.date() だと「Date型じゃない」と怒られますが、z.coerce.date() なら自動で変換してくれます。必須テクニックです。
高度な用法:画像検証
Astroは画像パスを検証する image() ヘルパーを提供しています。
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
schema: ({ image }) => z.object({ // 関数の形にする点に注意
title: z.string(),
cover: image(), // 画像パスを検証
}),
});これで、frontmatterの cover フィールドに指定された画像が存在するかどうかをチェックしてくれます。相対パスもOKです。
リレーション:z.reference()
記事とカテゴリー、記事と著者のような関連付けには z.reference() を使います。
// カテゴリーコレクション
const categoryCollection = defineCollection({
schema: z.object({
name: z.string(),
}),
});
// ブログコレクションで参照
const blogCollection = defineCollection({
schema: z.object({
title: z.string(),
category: z.reference('category'), // 'category'コレクションを参照
}),
});記事のfrontmatterではファイル名(slug)を指定します:
category: "tech" # src/content/category/tech.md を参照参照先が存在しない場合、エラーになります。
複雑なネスト
オブジェクトをネストさせることも可能です。
schema: z.object({
title: z.string(),
author: z.object({
name: z.string(),
email: z.string().email(), // メール形式チェック
avatar: z.string().url(), // URL形式チェック
}),
})getEntry() vs getCollection()
getCollection('blog'):全件取得。一覧ページ用。getEntry('blog', 'my-post'):Slug指定で1件取得。詳細ページ用。
詳細ページでの使用例:
---
// src/pages/blog/[slug].astro
import { getEntry } from 'astro:content';
const { slug } = Astro.params;
const post = await getEntry('blog', slug);
if (!post) {
return Astro.redirect('/404');
}
const { Content } = await post.render();
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>よくあるエラーと対処法
私がドハマりしたエラーたちです。これを知っていれば時間を無駄にせずに済みます。
エラー1:MarkdownContentSchemaValidationError
最もよく見るエラーです。「frontmatterがSchemaと合ってないよ」と言っています。
blog → my-post.md frontmatter does not match collection schema.
- "title" is required
- "pubDate" must be a valid date対策:
- 必須項目の漏れ:Schemaで
.optional()を付け忘れていませんか? - スペルミス:
pubDateをpublishDateと書いていませんか? - 型不一致:数値型なのに文字列を渡していませんか?
エラー2:InvalidContentEntryFrontmatterError
YAMLの構文自体が間違っていて解析できない状態です。
title: "引用符を閉じ忘れたタイトル対策:エディタのYAMLハイライト機能を活用しましょう。
エラー3:日付フォーマットのエラー
z.date() を使っているのに、frontmatterで文字列の日付を書いてエラーになるパターン。
対策:z.coerce.date() を使いましょう。これ一択です。
レガシーデータの移行:.passthrough()
古い記事がたくさんあってfrontmatterがバラバラな場合、一時的に検証を緩めることができます。
schema: z.object({
title: z.string(),
}).passthrough() // 未定義のフィールドがあっても許可するただし、これはあくまで一時しのぎです。将来的にはSchemaを厳格に定義することをお勧めします。
チェックリスト(トラブルシューティング用)
-
src/content/ディレクトリはあるか? -
src/content.config.tsの場所は正しいか? - エクスポートする
collectionsのキー名はディレクトリ名と一致しているか? - YAML構文は正しいか?
- 必須フィールドはすべて埋まっているか?
まとめ
Content Collectionsとは?
Markdownに型安全をもたらす機能。ビルド時にエラーを検出し、安全なコンテンツ管理を実現します。
設定方法は?
src/content/ディレクトリ作成src/content.config.tsでZod Schema定義defineCollection()でコレクション作成
Schema検証のコツz.coerce.date() は必須級。画像は image()、リレーションは z.reference() を活用。
初期設定は少し手間に感じるかもしれませんが、その見返りは絶大です。バグの温床となるfrontmatterのミスを根絶し、快適な開発体験を手に入れましょう。まだ使っていないなら、ぜひ次のプロジェクトで導入してみてください。
Astro Content Collections完全設定フロー
ディレクトリ作成からZodスキーマ定義、データ取得までの完全ガイド
⏱️ Estimated time: 30 min
- 1
Step1: ディレクトリ構造の作成
プロジェクトルートに src/content/ を作成します。
v2.0以降、これはAstroの予約ディレクトリです。
content/ 配下にサブディレクトリ(例:blog/)を作成し、それが「コレクション」となります。
注:設定ファイルは content/ の中ではなく、src/content.config.ts に配置します。 - 2
Step2: 設定ファイル (src/content.config.ts) の作成
必要なモジュールをインポートし、コレクションを定義します。
import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
pubDate: z.coerce.date(), // 文字列を日付型へ自動変換
tags: z.array(z.string()).optional(),
draft: z.boolean().default(false)
})
});
export const collections = {
'blog': blogCollection // キー名はディレクトリ名と一致させる
}; - 3
Step3: コンテンツの作成
src/content/blog/ 配下にMarkdownファイルを作成します。
frontmatterは定義したSchemaに従う必要があります。
---
title: "記事タイトル"
pubDate: "2024-12-01"
--- - 4
Step4: コンポーネントでのデータ取得
Astroファイル内で getCollection を使用してデータを取得します。
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog');
取得したデータ(post.data)は完全に型付けされており、エディタの補完が効きます。 - 5
Step5: 高度なSchema設定
画像検証:
schema: ({ image }) => z.object({ cover: image() })
他コレクションの参照:
category: z.reference('category')
オブジェクトのネスト:
author: z.object({ name: z.string(), email: z.string().email() }) - 6
Step6: エラー対処
よくあるMarkdownContentSchemaValidationErrorは、必須フィールドの欠落や型不一致が原因です。日付にはz.coerce.date()を使用することで、文字列の日付によるエラーを回避できます。
FAQ
Content Collectionsを使うメリットは?
Markdownのfrontmatterの記述ミス(スペルミス、日付フォーマット違い、必須項目漏れ)をビルド時に検出できます。
また、VS Codeなどのエディタでフィールド名の自動補完が効くようになり、開発効率が大幅に向上します。
設定ファイルはどこに置くべきですか?
よくある間違いとして `src/content/config.ts` に置いてしまうことがありますが、設定ファイルは `content` ディレクトリの中ではなく、`src` ディレクトリ直下に置く必要があります。
日付フィールドでバリデーションエラーが出ます。
Schemaで `z.date()` を使うと、Dateオブジェクトを期待するためエラーになります。
代わりに `z.coerce.date()` を使用してください。これを使うと、文字列の日付を自動的にDateオブジェクトに変換して検証してくれます。
既存の大量の記事があり、Schemaに合わないものが多い場合は?
`schema: z.object({...}).passthrough()` とすることで、Schemaで定義されていないフィールドが含まれていてもエラーにならず、検証を緩めることができます。ただし、長期的にはSchemaに合わせてデータを修正することを推奨します。
getCollectionとgetEntryの違いは?
`getEntry('blog', 'slug')` は指定したslug(ファイル名)を持つ**単一の記事**を返します。個別記事ページでのデータ取得にはこちらの方が効率的です。
画像をfrontmatterで扱う際のベストプラクティスは?
`schema: ({ image }) => z.object({ cover: image() })` のように定義すると、Astroは指定されたパスに画像が実在するか、正しい画像ファイルかをビルド時に検証してくれます。
4 min read · 公開日: 2025年11月24日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます