言語を切り替える
テーマを切り替える

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

ブログのトップページがクラッシュし、ある記事の publishDate フィールドの形式が正しくないとエラーが出ました。ファイルを 1 つずつ確認し、ようやく原因を特定するまでに 30 分かかりました。日付が 2024-12-01 ではなく 2024/12/01 になっていたのです。記事は 30 本ほどの小さなブログでしたが、100 本を超えると、フィールドを追加するたびに全ファイルを手作業で確認するのは現実的ではありません。

Content Collections はまさにこの問題を解決します。TypeScript がコードの誤りを検知するように、Astro がコンテンツのエラーを自動検出します。Schema を設定すれば、エディタでフィールド名の補完が効き、ドキュメントを開き直す必要も減ります。本記事では、Content Collections とは何か、設定ファイルの書き方、Schema 検証の使い方を解説します。

Content Collections とは?なぜ必要なのか?

「Markdown をフォルダ管理するのと何が違うの?」と思うかもしれません。src/pages/ 配下に blog/ を置けば、機能的にはブログは作れます。

しかし、型安全の保護がありません

従来の方法では、frontmatter は次のような形です:


---

title: "私のブログタイトル"
date: "2024-12-01"
tags: ["Astro", "チュートリアル"]

---

記事本文...

一見問題なさそうですが、次のようなミスは起きやすいものです:

  • ある記事で tagstag と書いてしまう(s が抜ける)
  • 日付を 2024-12-01 ではなく 12/01/2024 と書く
  • 新しく author フィールドを追加したが、古い記事に追記し忘れる

これらのエラーは、Astro が事前に教えてくれません。実行時にページが壊れて初めて気づくのです。

Content Collections はこれを解決します。 型安全なコンテンツ管理システムで、Markdown に TypeScript の型チェックを付けるイメージです。

具体的には次の能力があります:

  1. Schema 検証:frontmatter の型と構造を定義し、違反は即エラー
  2. 自動型生成:Schema から TypeScript 型を生成し、エディタ補完が効く
  3. 統一クエリ APIgetCollection() などで型安全にデータ取得
  4. パフォーマンス:Astro 5.0 の Content Layer API でクエリが高速化

要するに、従来は「自由だが不安」、Content Collections は「制約があるが信頼できる」。Schema 設定に少し時間をかければ、凡ミスの多くを防げます。

率直に言うと、私は今の Astro プロジェクトすべてで Content Collections を使っています。一度設定すれば、プロジェクト全体で恩恵があります。

Content Collections 設定の実践

理論はここまでにして、実際に設定しましょう。流れは 3 ステップです。ディレクトリ作成、設定ファイル作成、コンテンツ作成。

ステップ 1:ディレクトリ作成

Content Collections では、コンテンツを src/content/ に置きます。v2.0 以降の Astro 予約ディレクトリです。

ディレクトリ構造の例:

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/ の中ではありません。最初は場所を間違えて、かなりハマりました。

各サブディレクトリが 1 つのコレクションです。src/content/blog/ が blog コレクション、src/content/docs/ が docs コレクションです。

ステップ 2:設定ファイル作成

src/content.config.ts を作成します。Content Collections の中核です。

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

// blog コレクションの定義
const blogCollection = defineCollection({
  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
export const collections = {
  'blog': blogCollection,  // キー名はディレクトリ名と一致
};

ポイントを分解します:

  1. defineCollection():コレクション設定を定義
  2. type: 'content':Markdown/MDX コレクションであることを指定
  3. schema:Zod で frontmatter の構造を定義
  4. collections オブジェクト:export するキー名はディレクトリ名と一致させる

schema では各フィールドを z.xxx() で定義します:

  • z.string():文字列
  • z.coerce.date():文字列を Date に自動変換
  • z.array(z.string()):文字列配列
  • .optional():省略可能
  • .default(false):デフォルト値

ステップ 3:コンテンツファイル作成

設定後、src/content/blog/ に Markdown を置きます:


---

title: "Astro Content Collections 入門"
description: "Content Collections の設定と使い方"
pubDate: "2024-12-01"
tags: ["Astro", "チュートリアル"]

---

記事本文...

frontmatter が Schema に合っていれば、Astro は正常に解析します。pubDate の形式が誤っているなど、違反があればビルド時にエラーになります。

ページでのデータ取得

設定後、任意の Astro ファイルでコンテンツを取得できます:


---

// src/pages/blog/index.astro
import { getCollection } from 'astro:content';

// 全ブログ記事を取得
const allPosts = await getCollection('blog');

// 下書き(draft: true)を除外
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 で、完全な TypeScript 型ヒントが付きます。VS Code で post.data. と入力すると、titledescriptionpubDate などが補完されます。

これが Content Collections の魅力です。型安全とエディタ補完で、コーディング体験が一段上がります。

Schema 検証を深掘りする

前節では z.string()z.coerce.date() などの基本型を使いました。Schema 検証にはもっと広い用途があります。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() は特に便利です。Markdown の frontmatter では日付は文字列("2024-12-01")として書くのが普通です。z.date() だと Date オブジェクトを要求するためエラーになりがちですが、z.coerce.date() なら自動変換してくれます。

任意フィールドとデフォルト値

すべてのフィールドが必須とは限りません。tags のように省略したい場合は .optional() を使います:

schema: z.object({
  title: z.string(),                    // 必須
  tags: z.array(z.string()).optional(), // 任意
  draft: z.boolean().default(false),    // デフォルトあり
})

.default() は、frontmatter に書かれていないときに Astro が自動で埋めてくれます。

高度な用法:画像検証

Astro は画像パス用の image() 型を提供しています:

import { defineCollection, z } from 'astro:content';

const blogCollection = defineCollection({
  schema: ({ image }) => z.object({  // 関数形式に注意
    title: z.string(),
    cover: image(),  // 画像パスを検証
  }),
});

image() は有効な画像ファイルへのパスか検証します(相対パスも可)。一覧ページでカバー画像を出すときに便利です。

他コレクションの参照:z.reference()

記事とカテゴリのように、コンテンツ同士を関連付けるときは z.reference() を使います:

// カテゴリコレクション
const categoryCollection = defineCollection({
  schema: z.object({
    name: z.string(),
    slug: z.string(),
  }),
});

// ブログがカテゴリを参照
const blogCollection = defineCollection({
  schema: z.object({
    title: z.string(),
    category: z.reference('category'),  // category コレクションを参照
  }),
});

export const collections = {
  'category': categoryCollection,
  'blog': blogCollection,
};

ブログの frontmatter では、カテゴリのファイル名(拡張子なし)を書きます:


---

title: "私のブログ"
category: "tech"  # src/content/category/tech.md を参照

---

参照先が存在しない場合はエラーになり、型も安全です。

複雑なオブジェクトのネスト

frontmatter が複雑な場合はオブジェクトをネストできます:

schema: z.object({
  title: z.string(),
  author: z.object({
    name: z.string(),
    email: z.string().email(),  // メール形式
    avatar: z.string().url(),   // URL 形式
  }),
  seo: z.object({
    keywords: z.array(z.string()),
    description: z.string().max(160),  // 最大長
  }).optional(),
})

対応する frontmatter:


---

title: "記事タイトル"
author:
  name: "张三"
  email: "zhangsan@example.com"
  avatar: "https://example.com/avatar.jpg"
seo:
  keywords: ["Astro", "チュートリアル"]
  description: "Astro に関するチュートリアル"

---

型安全の仕組み:TypeScript の自動推論

Schema を設定すると、Astro が TypeScript 型を自動生成します。データ取得時にエディタ補完が効きます:

import { getCollection } from 'astro:content';

const posts = await getCollection('blog');

posts.forEach(post => {
  // post.data のフィールドが補完される
  console.log(post.data.title);       // ✅ 型: string
  console.log(post.data.pubDate);     // ✅ 型: Date
  console.log(post.data.tags);        // ✅ 型: string[] | undefined
  console.log(post.data.notExist);    // ❌ コンパイルエラー:存在しないフィールド
});

手動で型定義を書かなくてよいのが嬉しいところです。Schema に沿った正確な型が常についてきます。

getEntry()getCollection() の違い

クエリ API の使い分け:

  • 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>

最初は Zod の構文に戸惑うかもしれませんが、数回使えば慣れます。エラーメッセージも分かりやすく、切り分けしやすいです。

よくある問題と解決策

Content Collections を設定すると、さまざまなエラーに出会うことがあります。ここではよくあるものと対処法をまとめます。私自身が踏んだ坑です。

エラー 1:MarkdownContentSchemaValidationError

最も多いエラーで、frontmatter が Schema に合っていないことを示します。メッセージの例:

blog → my-post.md frontmatter does not match collection schema.
- "title" is required
- "pubDate" must be a valid date

エラーの読み方

Astro は、どのファイル(my-post.md)のどのフィールド(titlepubDate)に問題があるかを明示します。

よくある原因と対処

  1. フィールド欠落:Schema で必須なのに frontmatter にない

    • 対処:フィールドを追加するか、Schema で .optional() にする
  2. フィールド名のスペルミスpubDatepublishDate と書くなど

    • 対処:名前を統一し、エディタの補完を使う
  3. 型不一致:Schema が z.number() なのに文字列を渡しているなど

    • 対処:値の形式を確認する

エラー 2:InvalidContentEntryFrontmatterError

frontmatter 自体の YAML 構文が壊れており、パースできない状態です。

よくある例:


---

title: "私のタイトル
description: "引用符を閉じ忘れた"

---

対処:引用符、コロン、インデントなど YAML 構文を確認してください。YAML 構文チェック付きのエディタプラグインがおすすめです。

エラー 3:日付形式の問題

z.date() を使うと、frontmatter の日付は Date オブジェクトである必要があります。しかし YAML では文字列しか書けません。ここで何度かハマりました。

対処:Schema では z.coerce.date() を使い、文字列を Date に自動変換します:

// ❌ 誤り:Date オブジェクトを要求するが frontmatter は文字列
pubDate: z.date()

// ✅ 正しい:文字列を Date に変換
pubDate: z.coerce.date()

レガシーデータ:.passthrough()

既存記事が多く、frontmatter がバラバラな場合、一時的に検証を緩められます:

schema: z.object({
  title: z.string(),
  // ... 他フィールド
}).passthrough()  // 未定義フィールドも許可

ただし一時しのぎです。長期的には frontmatter を統一することをおすすめします。

複数コレクションの構成

ブログ、ドキュメント、事例など種類が分かれる場合は、複数コレクションを作ります:

src/content/
├── blog/
├── docs/
└── case-studies/

content.config.ts で個別に定義します:

const blogCollection = defineCollection({ /* ... */ });
const docsCollection = defineCollection({ /* ... */ });
const caseStudiesCollection = defineCollection({ /* ... */ });

export const collections = {
  'blog': blogCollection,
  'docs': docsCollection,
  'case-studies': caseStudiesCollection,
};

各コレクションは異なる Schema を持て、互いに干渉しません。

Schema 設計のベストプラクティス

経験からのまとめ:

  1. 必須フィールドは最小限:本当に必要なものだけ必須にし、他は .optional().default()
  2. 日付は z.coerce.date():手動変換の手間を省く
  3. フィールド名はキャメルケースpubDatepub_date より JavaScript の慣習に合う
  4. 複雑なオブジェクトは分割:frontmatter が肥大化するなら、コレクションを分けて z.reference() で関連付け
  5. Schema にコメント:各フィールドの用途をチームに伝える

チェックリスト(トラブル時)

エラー時は次の順で確認してください:

  • src/content/ ディレクトリは存在するか?
  • src/content.config.ts の位置は正しいか?(content/ の中ではない)
  • collections のキー名はディレクトリ名と一致しているか?
  • frontmatter の YAML 構文は正しいか?(引用符、コロン、インデント)
  • 必須フィールドはすべて埋まっているか?
  • フィールドの型は Schema と一致しているか?

一見たくさんありますが、Astro のエラーメッセージは親切なので、内容を読めば多くの場合すぐに原因にたどり着けます。

まとめ

最初の 3 つの悩みに戻りましょう。

Content Collections とは? Markdown に TypeScript の型チェックを付ける仕組みです。ビルド時にエラーを検出し、ページが壊れてから気づく事態を減らせます。

設定ファイルの書き方は? 3 ステップを覚えてください。src/content/ を作り、src/content.config.tsdefineCollection() と Zod により Schema を定義。export するキー名はディレクトリ名と一致させます。

Schema 検証の使い方は? 基本型(z.string()z.coerce.date()z.array())と、.optional().default() を押さえ、エラーは Astro のメッセージを手がかりに切り分けます。

率直に言うと、Content Collections は Astro で最も使う価値がある機能のひとつです。初期設定に時間はかかりますが、その後のデバッグ時間を大きく減らせます。エディタ補完の快適さも、一度体験すると戻りにくいです。

次のステップ

今すぐ試すなら、次の進め方がおすすめです:

  1. 新規プロジェクトから:Astro プロジェクト作成時に Content Collections を入れ、最初から規律を作る
  2. 既存サイトは段階的に:まず .passthrough() で動かし、徐々に frontmatter を統一する
  3. 公式ドキュメントを参照Astro 公式ドキュメント に API リファレンスがあります

難しくはありませんが、手を動かすことが大切です。設定ファイルを 1 回書いてみてください。型安全なコンテンツ管理の心地よさがわかるはずです。

Astro Content Collections の完全設定フロー

Content Collections のゼロからの設定から Schema 検証まで、型安全なコンテンツ管理システムを構築する手順

⏱️ 目安時間: 30 分

  1. 1

    ステップ1: ディレクトリ構造の作成

    プロジェクトルートに src/content/ ディレクトリを作成します。
    • v2.0 以降の Astro 予約ディレクトリ
    • content/ 配下のサブディレクトリ(blog/、docs/ など)がそれぞれ 1 つのコレクション

    注意:設定ファイル src/content.config.ts は content/ の中ではなく src/ 直下に置きます。
  2. 2

    ステップ2: 設定ファイルの作成

    src/content.config.ts を作成します。

    1. 依存関係の import:
    import { defineCollection, z } from 'astro:content'

    2. コレクション定義:
    const blogCollection = defineCollection({
    type: 'content',
    schema: z.object({
    title: z.string(),
    description: z.string(),
    pubDate: z.coerce.date(),
    tags: z.array(z.string()).optional(),
    draft: z.boolean().default(false)
    })
    })

    3. collections オブジェクトを export:
    export const collections = { 'blog': blogCollection }
    キー名はディレクトリ名と一致させる必要があります
  3. 3

    ステップ3: コンテンツファイルの作成

    src/content/blog/ 配下に Markdown ファイルを作成します。

    frontmatter は Schema 定義に従う必要があります:
    • title(必須の文字列)
    • description(必須の文字列)
    • pubDate("2024-12-01" 形式。z.coerce.date() が自動変換)
    • tags(任意の文字列配列)
    • draft(任意の真偽値、デフォルト false)

    フィールドが Schema に合わない場合、Astro はビルド時にエラーを出します。
  4. 4

    ステップ4: ページでのデータ取得

    Astro ファイルで getCollection を import:
    import { getCollection } from 'astro:content'

    全記事の取得:
    const allPosts = await getCollection('blog')

    下書きの除外:
    const publishedPosts = allPosts.filter(post => !post.data.draft)

    データの利用:
    • post.data に完全な TypeScript 型ヒント
    • エディタが title、description、pubDate などを補完
    • 単一記事は getEntry('blog', slug) の方が効率的
  5. 5

    ステップ5: 高度な Schema 設定

    画像検証:
    schema: ({ image }) => z.object({
    cover: image()
    })

    他コレクションの参照:
    z.reference('category')

    オブジェクトのネスト:
    z.object({
    author: z.object({
    name: z.string(),
    email: z.string().email(),
    avatar: z.string().url()
    })
    })

    レガシーデータ:
    .passthrough() で未定義フィールドを一時的に許可

    複数コレクション:
    collections オブジェクトで blog、docs、case-studies などを個別に定義
  6. 6

    ステップ6: よくあるエラーの切り分け

    MarkdownContentSchemaValidationError:フィールド欠落(追加するか .optional())、スペルミス(フィールド名を統一)、型不一致(値の形式を確認)。InvalidContentEntryFrontmatterError:YAML 構文(引用符、コロン、インデント)を確認。日付:z.coerce.date() を使い z.date() は避ける。チェックリスト:content/ の有無、content.config.ts の位置、collections のキー名とディレクトリ名の一致、YAML 構文、必須フィールド、型の一致。

FAQ

Content Collections とは?なぜ必要なのですか?
Content Collections は型安全なコンテンツ管理システムで、本質的には Markdown に TypeScript の型チェックを付ける仕組みです。

従来の方法(src/pages/ 直下に blog/ フォルダを置く)には型安全の保護がありません。よくある問題:
• フィールドのスペルミス(tags を tag と書く)
• 日付形式の誤り(12/01/2024 ではなく 2024-12-01)
• 新フィールド追加時に古い記事へ未反映
• Astro は事前に教えてくれず、実行時にページが壊れて初めて気づく

Content Collections が提供するもの:
1) Schema 検証(frontmatter の型と構造を定義し、違反は即エラー)
2) 自動型生成(Schema から TypeScript 型を生成、エディタ補完)
3) 統一クエリ API(getCollection() などが型安全なデータを返す)
4) パフォーマンス(Astro 5.0 の Content Layer API でクエリが高速化)

従来は「自由だが不安」、Content Collections は「制約があるが信頼できる」。一度設定すればプロジェクト全体で恩恵を受けられます。
Content Collections の設定手順は?
設定は 3 ステップです。

1) ディレクトリ作成:
• プロジェクトルートに src/content/(v2.0 以降の予約ディレクトリ)
• content/ 配下に blog/、docs/ などのサブディレクトリ。各サブディレクトリが 1 コレクション

2) 設定ファイル:
• src/content.config.ts を作成(content/ の中ではない)
• defineCollection と z を import
• type: 'content' で Markdown/MDX、schema で Zod により frontmatter を定義
• collections を export(キー名はディレクトリ名と一致、例:'blog': blogCollection)

3) コンテンツ作成:
• src/content/blog/ に Markdown を配置
• frontmatter は Schema に従う。違反があればビルド時にエラー
Schema 検証の使い方とよく使う型は?
Zod で型を定義します:
• z.string() 文字列
• z.number() 数値
• z.boolean() 真偽値
• z.date() Date オブジェクト
• z.coerce.date() 文字列を Date に自動変換(YAML では文字列しか書けないので超便利)
• z.array(z.string()) 文字列配列
• z.enum(['draft', 'published']) 列挙型

任意フィールドとデフォルト:
• .optional() で省略可能
• .default(false) でデフォルト値

高度な用法:
• image() で画像パス検証:schema: ({ image }) => z.object({ cover: image() })
• z.reference('category') で他コレクション参照
• ネスト:z.object({ author: z.object({ name: z.string(), email: z.string().email() }) })

Schema 設定後、Astro が TypeScript 型を自動生成し、post.data の全フィールドが型安全になります。
getCollection と getEntry の違いは?データの取得方法は?
クエリ API:
• getCollection('blog') はコレクション全体を配列で返す
• getEntry('blog', 'my-post') は slug 指定の 1 件を返す(詳細ページ向けでより効率的)

Astro ファイルでの import:
import { getCollection, getEntry } from 'astro:content'

利用:
• post.data が frontmatter で、完全な TypeScript 型ヒント付き
• エディタが title、description、pubDate などを補完

下書き除外:
const publishedPosts = allPosts.filter(post => !post.data.draft)

単一記事の例:
const post = await getEntry('blog', slug)
if (!post) return Astro.redirect('/404')
const { Content } = await post.render()
Content Collections でよくあるエラーと解決策は?
よくあるエラー:

1) MarkdownContentSchemaValidationError(frontmatter が Schema に合わない):
• 必須フィールド欠落(追加するか .optional())
• フィールド名のスペルミス(統一しエディタ補完を使う)
• 型不一致(値の形式を確認)

2) InvalidContentEntryFrontmatterError(YAML 構文エラー):
• 引用符、コロン、インデントを確認
• YAML 構文チェック付きエディタプラグインを推奨

3) 日付形式:
• z.coerce.date() を使う(z.date() は YAML の文字列日付で失敗しやすい)

4) レガシーデータ:
• .passthrough() で一時的に緩和可能だが長期的には frontmatter を統一

チェックリスト:
• content/ の有無
• content.config.ts の位置(content/ 内ではない)
• collections のキー名とディレクトリ名の一致
• YAML 構文
• 必須フィールドの充足
• 型の一致
複数コレクションの構成と Schema 設計のベストプラクティスは?
複数コレクション:
• ブログ、ドキュメント、事例などがある場合、src/content/blog/、docs/、case-studies/ などを作成
• content.config.ts で個別に定義:
const blogCollection = defineCollection({...})
const docsCollection = defineCollection({...})
• export:
'blog': blogCollection,
'docs': docsCollection,
'case-studies': caseStudiesCollection
• 各コレクションは異なる Schema を持てる

Schema 設計のベストプラクティス:
1) 必須フィールドは最小限(本当に必要なものだけ必須、他は .optional() か .default())
2) 日付は z.coerce.date()(手動変換の手間を省く)
3) フィールド名はキャメルケース(pubDate は pub_date より JS 慣習に合う)
4) 複雑なオブジェクトは分割(frontmatter が複雑なら複数コレクション + z.reference())
5) Schema にコメントで各フィールドの用途を明記

4分で読めます · 公開日: 2025年11月24日 · 更新日: 2026年6月8日

関連記事

コメント

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