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

ネットの記事を真似したら src/content/config.ts がないって言われた…
Astroで記事を扱うときの標準的な仕組みが コンテンツコレクション です。
ざっくり言うと、記事ファイルの置き場所と「持っていい項目」をコードで宣言しておく仕組みです。
ただ、この機能は Astro v5 で書き方が大きく変わりました。
検索して出てくる情報が古いままだったりして、そこそこ混乱しやすいんですよね。
この記事では、今のAstroでのコンテンツコレクションの書き方を、設定ファイルから記事の取り出しまで順番に整理していきます。
- コンテンツコレクションが何を解決してくれる仕組みなのか
src/content.config.tsでのローダーとスキーマの書き方getCollection/renderで記事を取り出して表示する流れ- 下書きの除外方法と、バージョンによって変わったポイント
なお、このブログ自体がAstroで動いています。乗り換えたときの話はこちらにまとめています。

- コードは Astro v7系(最新は7.2.0) の公式ドキュメントに沿った書き方で載せています
- ただし このブログ自体はまだ v5.16系 で動かしています
- v5とv7で書き方が違う箇所は、そのつど本文で明記します
公式の解説は Content collections にあります。
Astroのコンテンツコレクションとは?
コンテンツコレクションとは、記事のようなファイル群を「コレクション」という単位でまとめて、型付きで扱えるようにするAstroの機能です。
やっていることは、大きく分けて2つだけです。
- どこにある何のファイルを読み込むかを決める(ローダー)
- その中身がどんな項目を持つかを決めて検証する(スキーマ)
この2つを設定ファイルに書いておくと、あとは getCollection("blog") のような関数を呼ぶだけで、型のついた記事一覧が手に入ります。
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 をどこから取るかが、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} のようにアンダースコア始まりを弾くパターンもよく使われます。
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.ts | src/content.config.ts |
| 読み込み元の指定 | type: "content" | loader: glob({ ... }) |
| 記事の識別子 | entry.slug | entry.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で削除済み
らるじゅ最初の設定ファイルさえ書いてしまえば、あとは記事を置くだけで増やせます。
仕組みを整えるほど書くのがラクになるので、ぜひ最初に手を入れてみてください!
