【Astro】コンテンツコレクションとは?記事を型安全に管理する書き方

【Astro】コンテンツコレクションとは?記事を型安全に管理する書き方

Astroでブログを作りたいけど、記事のデータってどう管理するのが正解なの?

ネットの記事を真似したら src/content/config.ts がないって言われた…

Astroで記事を扱うときの標準的な仕組みが コンテンツコレクション です。
ざっくり言うと、記事ファイルの置き場所と「持っていい項目」をコードで宣言しておく仕組みです。

ただ、この機能は Astro v5 で書き方が大きく変わりました。
検索して出てくる情報が古いままだったりして、そこそこ混乱しやすいんですよね。

この記事では、今のAstroでのコンテンツコレクションの書き方を、設定ファイルから記事の取り出しまで順番に整理していきます。

この記事でわかること
  • コンテンツコレクションが何を解決してくれる仕組みなのか
  • src/content.config.ts でのローダーとスキーマの書き方
  • getCollection / render で記事を取り出して表示する流れ
  • 下書きの除外方法と、バージョンによって変わったポイント

なお、このブログ自体がAstroで動いています。乗り換えたときの話はこちらにまとめています。

この記事の前提(2026年8月時点)
  • コードは Astro v7系(最新は7.2.0) の公式ドキュメントに沿った書き方で載せています
  • ただし このブログ自体はまだ v5.16系 で動かしています
  • v5とv7で書き方が違う箇所は、そのつど本文で明記します

公式の解説は Content collections にあります。

Astroのコンテンツコレクションとは?

コンテンツコレクションとは、記事のようなファイル群を「コレクション」という単位でまとめて、型付きで扱えるようにするAstroの機能です。

やっていることは、大きく分けて2つだけです。

コンテンツコレクションがやってくれること
  • どこにある何のファイルを読み込むかを決める(ローダー)
  • その中身がどんな項目を持つかを決めて検証する(スキーマ)

この2つを設定ファイルに書いておくと、あとは getCollection("blog") のような関数を呼ぶだけで、型のついた記事一覧が手に入ります。

フロントマター(frontmatter)

MarkdownやMDXファイルの先頭に --- で挟んで書く、タイトルや日付などのメタ情報のことです。コンテンツコレクションが検証してくれるのは、主にこの部分になります。

自分でファイルを読むのと何が違う?

Astroには import.meta.glob() でファイルをまとめて読む方法もあります。ではなぜコレクションを使うのか。

一番大きいのは、typoや書き忘れがビルド時に落ちることだと思います。

たとえば pubDate を書き忘れた記事があったとき、自前で読み込む方式だと、たいてい公開後にページが崩れて初めて気づくことになります。
コレクションなら、その記事が原因でビルドが失敗するので、公開前に止まってくれます。

らるじゅらるじゅ

「本番で崩れて気づく」が一番しんどいんですよね。
ビルドで止まってくれるのは、うるさいようでいてかなりありがたい。

コレクションを定義する|src/content.config.ts の書き方

まずは設定ファイルを作ります。置き場所は src/content.config.ts です。

設定ファイルの場所に注意

Astro v4以前は src/content/config.ts(contentフォルダの中)でした。v5以降は src/content.config.ts(srcの直下) に変わっています。ここを間違えると、設定が読まれずコレクションが見つからないエラーになります。

全体像はこんな形になります。私がこのブログで使っている設定を、最新版の書き方に合わせて簡略化したものです。

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

const blog = defineCollection({
  // どのファイルを読むか
  loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
  // どんな項目を持つか
  schema: z.object({
    title: z.string(),
    description: z.string().min(10).max(160),
    pubDate: z.coerce.date(),
    updatedDate: z.coerce.date().optional(),
    draft: z.boolean().default(false),
    category: z.array(z.string()),
    tags: z.array(z.string()).default([]),
  }),
});

export const collections = { blog };

最後の export const collections を書き忘れると、コレクションとして登録されません。地味ですが必須です。

z のインポート元はバージョンで違う

スキーマに使う z をどこから取るかが、v6で整理されました。

  • v5時代のサンプル … import { defineCollection, z } from "astro:content" でまとめて取得する書き方が主流
  • v6以降の推奨 … z は astro/zod から別途インポート(astro:content の z と astro:schema は非推奨)

astro/zod からのインポートは v5でもそのまま動きます。新しく書くなら最初からこちらに寄せておけば、あとで直す手間がありません。古いサンプルを読むときだけ、取得元が違うことを頭に置いておけば大丈夫です。

ローダー:どのファイルを読み込むか

loader は、読み込み元を決めるパーツです。ファイルベースで書くなら astro/loaders の glob() を使います。

loader: glob({ base: "./src/content/blog", pattern: "**/*.{md,mdx}" }),
  • base … 探し始めるフォルダ
  • pattern … 拾うファイルのパターン

**/*.{md,mdx} は「配下のすべてのフォルダから、拡張子が .md または .mdx のファイルを拾う」という意味です。
記事ごとにフォルダを切って、その中に画像も一緒に置く構成でも、これで問題なく読み込めます。

下書きをファイル名で除外したい場合は、**/[^_]*.{md,mdx} のようにアンダースコア始まりを弾くパターンもよく使われます。

loaderは他にもある

1つのJSONやCSVをまとめて読む file() ローダーや、外部APIから取ってくる独自ローダーも定義できます。「ファイル以外もコレクションにできる」のが、この仕組みの大きな特徴です。

スキーマ:フロントマターの型を決める

schema は、フロントマターの検証ルールです。
スキーマ定義には Zod というバリデーションライブラリを使いますが、Astroに同梱されているので追加インストールは不要です。

よく使う書き方をまとめておきます。

書き方意味
z.string()必須の文字列
z.string().optional()省略してよい文字列
z.string().min(10).max(160)10〜160文字の文字列
z.coerce.date()日付。2026-08-10 のような文字列も日付に変換される
z.boolean().default(false)省略したら false になる真偽値
z.array(z.string())文字列の配列

私が一番効いていると感じるのは min() / max() です。

たとえば description に .min(10).max(160) を付けておくと、meta descriptionが長すぎる記事はビルドで弾かれます。
SEO上の指摘を、レビューではなく仕組みで防げるわけです。

画像を検証したいとき

schema を関数形式にすると image ヘルパーが使えます。パスの間違いをビルド時に検出できます。

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

定義したコレクションを取り出す|getCollection と render

設定ができたら、あとは呼ぶだけです。使う関数は主に3つです。

関数用途
getCollection()コレクション全体を取得する(一覧ページ向け)
getEntry()IDを指定して1件だけ取得する
render()記事の本文をレンダリングする

一覧ページ:getCollection

記事一覧は getCollection() で取ります。第2引数に絞り込み用の関数を渡せます。

---
import { getCollection } from "astro:content";

// 下書きを除いて、新しい順に並べる
const posts = (await getCollection("blog", ({ data }) => data.draft !== true)).sort(
  (a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
);
---

<ul>
  {posts.map((post) => (
    <li>
      <a href={`/blog/${post.id}/`}>{post.data.title}</a>
    </li>
  ))}
</ul>

ここで大事なのが .sort() を自分で書いていることです。

公式ドキュメントにも明記されていますが、getCollection() が返す順番は非決定的で、実行環境によって変わります。日付順に並べたいなら、自分で並べ替えます。

らるじゅらるじゅ

ローカルでたまたま正しい順に見えていて、ビルドしたら順番が変わる。
これに気づかないと原因を探すのに時間を溶かします。

記事ページ:getStaticPaths と render

記事の詳細ページは、動的ルート([...slug].astro など)と組み合わせます。

---
import { getCollection, render } from "astro:content";

export async function getStaticPaths() {
  const posts = await getCollection("blog");
  return posts.map((post) => ({
    params: { slug: post.id },
    props: post,
  }));
}

const post = Astro.props;
const { Content, headings } = await render(post);
---

<h1>{post.data.title}</h1>
<Content />

render() が返す Content が本文のコンポーネントです。headings には見出しの一覧が入るので、目次を自作するときはこれをそのまま使えます。

フロントマターの値は post.data の下に入っています。post.data.title のように書くと、エディタで補完が効きます。
スキーマに書いていない項目を触ろうとすると、その場で型エラーになるのが気持ちいいところです。

下書きを本番だけ非公開にする

個人ブログだと、書きかけの記事はローカルでは見たいが、本番には出したくないというケースが多いと思います。

スキーマに draft を持たせておけば、取得時の絞り込みで切り替えられます。

---
import { getCollection } from "astro:content";

const posts = await getCollection("blog", ({ data }) => {
  // 本番ビルドのときだけ下書きを除外する
  return import.meta.env.PROD ? data.draft !== true : true;
});
---

import.meta.env.PROD は本番ビルド時に true になる変数です。
これで開発サーバーでは下書きも見えて、公開時には消えるという挙動になります。

注意点として、この絞り込みは呼び出しているファイルにしか効きません。一覧ページだけ除外して詳細ページの getStaticPaths() に入れ忘れると、リンクは消えているのにURLを直接叩けば読めるという状態になります。除外するなら両方に入れておきましょう。

コンテンツコレクションでつまずきやすい5つのポイント

古い情報と混ざりやすいところを、まとめて整理しておきます。

項目v4以前(旧API)v5以降(Content Layer API)
設定ファイルsrc/content/config.tssrc/content.config.ts
読み込み元の指定type: "content"loader: glob({ ... })
記事の識別子entry.slugentry.id
本文の描画entry.render()render(entry)
置き場所src/content/ 固定base で自由に指定

なお v6で旧APIは完全に削除されました。互換のためのフラグも残っていません。
つまり今のAstroでは、右側の書き方が「新しいやり方」ではなく唯一のやり方です。迷わず新しい方を覚えて大丈夫です。

1. 設定ファイルの場所が変わっている

前述のとおり、src/content/config.ts ではなく src/content.config.ts です。
移行時にファイルを移し忘れると、コレクションが空のまま動いてしまうことがあります。

2. z のインポート元がバージョンで違う

v5時代のサンプルは astro:content から z をまとめてインポートしています。
v6以降は astro/zod から インポートするのが推奨です。astro/zod はv5でも使えるので、新規で書くなら最初からこちらに寄せておくと後で直す手間がありません。

3. slug ではなく id を使う

v4以前の記事は post.slug を使っています。v5以降は post.id です。

glob() ローダーの場合、ファイル名をもとにURLに使いやすい形へ変換された値が id になります。パスがそのまま入るわけではなく、大文字が小文字に直るなどの正規化が入る点は覚えておくとよいです。
変換ルールを変えたいときは generateId オプションで差し替えられます。

4. 本文の描画は render(entry) の形

await entry.render() は、import { render } from "astro:content" した上で await render(entry) と書く形に変わりました。エラーメッセージが分かりにくい箇所なので、古いサンプルを写したときは真っ先に疑うポイントです。

5. スキーマ違反はビルドが止まる(そういう仕様)

description の文字数オーバーや pubDate の書き忘れがあると、ビルドがエラーで止まります。
これは不具合ではなく、そのために型を書いているので、エラーメッセージに出ているファイル名と項目名を直せばOKです。

どこから手をつければいい?
  • これから作る … npm create astro@latest で入る最新版なら、この記事の書き方がそのまま使えます
  • v4以前から移行する … 設定ファイルの移動 → loader の追加 → slug/render の置き換え、の順が安全です
  • サンプルを写している … その記事が新しいかどうかは、content.config.ts の位置と z のインポート元で見分けられます

こうした設定まわりの作業をAIに任せている開発環境については、別の記事にまとめています。

まとめ|コンテンツコレクションは「壊れ方」を先に決める仕組み

コンテンツコレクションは、記事を読み込むための便利機能というより、記事の壊れ方を先に決めておく仕組みだと考えると腑に落ちやすいと思います。

項目を書き忘れたら公開前に止まる。型が違えば補完が効かない。
ひとりで運営していると、このガードレールがそのままレビュアーの代わりになってくれます。

この記事のまとめ
  • コンテンツコレクションは ローダー(どこを読むか) と スキーマ(何を持つか) の宣言
  • 設定ファイルはv5以降 src/content.config.ts(srcの直下)
  • 一覧は getCollection()、本文は render()。並び順は自分でsortする
  • draft + import.meta.env.PROD で下書きを本番だけ非公開にできる
  • v4以前とは slug → id、entry.render() → render(entry) が変わり、旧APIはv6で削除済み
らるじゅらるじゅ

最初の設定ファイルさえ書いてしまえば、あとは記事を置くだけで増やせます。
仕組みを整えるほど書くのがラクになるので、ぜひ最初に手を入れてみてください!