- 著者
- app-hacks
- 公開日
- 更新日
- 確認日
Astro / TypeScript / Content Collections
Astro Content Collections入門:Zodを使った堅牢な型安全ブログの構築
Astro 7のContent CollectionsでMarkdownのFrontmatterを検証し、型安全に記事一覧を取得する実装を整理する。
AstroでMarkdown記事を管理するとき、ファイルを単純に読み込むだけではFrontmatterの欠落や型の揺れをビルド前に検出しにくい。たとえば公開日をある記事では文字列、別の記事ではDateとして扱うと、並び替えや日付表示の処理に条件分岐が増える。Content Collectionsは、コンテンツの読み込み方法とメタデータのスキーマを一か所に定義し、この揺れを境界で止める仕組みである。
対象バージョンと確認範囲
この記事は2026年9月11日に、このリポジトリへ導入されているAstro 7.3.2とNode.js 22.22.2で確認した。実際のsrc/content.config.tsでは、glob()ローダーでsrc/content/blog配下のMarkdownを読み込み、ZodスキーマでFrontmatterを検証している。外部CMSやMDXはこの確認範囲に含めていない。
コレクションを定義する
Astro 7では、ビルド時コレクションをsrc/content.config.tsで定義する。ローカルのMarkdownを記事ごとのファイルとして置く場合は、astro/loadersが提供するglob()ローダーを使う。スキーマにはastro/zodから読み込んだzを使用する。
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',
}),
schema: z.object({
title: z.string().min(1),
description: z.string().min(1),
pubDate: z.coerce.date(),
author: z.string().min(1).optional(),
updatedDate: z.coerce.date().optional(),
reviewedAt: z.coerce.date().optional(),
tags: z.array(z.string().min(1)).min(1),
}),
});
export const collections = { blog };
baseは記事を置くディレクトリ、patternは対象ファイルを示す。対象をMarkdownだけに限定するなら**/*.mdで十分である。将来MDXを導入したときだけ**/*.{md,mdx}へ広げればよく、使っていない形式を先回りして含める必要はない。
z.string().min(1)は空文字を拒否する。単なるz.string()では空のタイトルも有効になるため、一覧にリンクテキストが出ない記事を作れてしまう。tagsも最低一件を要求し、空文字のタグを拒否する。制約は実際の表示要件から逆算して置くことが重要である。
pubDateにはz.coerce.date()を指定している。MarkdownのFrontmatterに書いた日付はパーサーの解釈を経るが、この指定によって最終的なコレクションデータはDateとして扱える。ページ側で毎回new Date()を呼び、変換結果を疑う必要がなくなる。
Frontmatterをスキーマに合わせる
記事側にはスキーマで必須とした四項目を書く。
---
title: "Astroで型安全な記事管理を始める"
description: "Content Collectionsの最小構成を解説する。"
pubDate: 2026-09-01
author: "app-hacks"
updatedDate: 2026-09-11
reviewedAt: 2026-09-11
tags: ["Astro", "TypeScript"]
---
項目名は完全一致が前提である。pubDateをpublishedAtと書き換えたり、tagsをカンマ区切りの単一文字列にしたりするとスキーマを通らない。スキーマエラーは不便な制約ではなく、壊れた記事が公開物へ混ざるのを防ぐ契約である。項目を追加したい場合も、まず全記事で本当に必要かを確認する。任意項目ならz.string().optional()のように明示し、既存記事を一斉に壊さない設計にする。
getCollectionで一覧を取得する
ページではgetCollection()を使って登録済みコレクションを読む。返り値の各要素にはid、data、本文情報があり、dataの型はスキーマから推論される。
---
import { getCollection } from 'astro:content';
const posts = (await getCollection('blog')).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>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString('ja-JP')}
</time>
</li>
))}
</ul>
ここではpubDateがDateと推論されるため、valueOf()やtoISOString()を安全に呼べる。存在しないpost.data.authorへアクセスすればTypeScriptが指摘する。型を手書きした配列へMarkdownを無理に当てはめる場合と違い、スキーマが実データの検証とコード上の型定義を兼ねる点が大きい。
絞り込み条件もgetCollection()の第二引数に置ける。下書き項目をスキーマへ追加したプロジェクトなら、getCollection('blog', ({ data }) => !data.draft)のように公開対象だけを読み込める。ただし、この例のスキーマにはdraftが存在しない。記事コードは実際のスキーマに存在する項目だけを前提にするべきである。
詳細ページを静的生成する
各記事のURLをビルド時に作るには、動的ルートのgetStaticPaths()で記事とパスを対応させる。本文のレンダリングはrender()へエントリーを渡して行う。
---
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 } = await render(post);
---
<article>
<h1>{post.data.title}</h1>
<Content />
</article>
この構成では、新しいMarkdownを追加するとローダーがエントリーを検出し、ビルドがFrontmatterを検証し、動的ルートがHTMLを生成する。ファイル名から得られるidをURLへ使うため、別のslugフィールドを重複管理しなくてよい。
検証を公開手順へ組み込む
スキーマはビルド時に初めて価値を発揮する。記事を追加したら開発画面だけで終えず、npm run buildを実行する。必須項目の欠落、日付変換の失敗、テンプレート側の型不整合を公開前にまとめて検出できる。CIでも同じコマンドを実行すれば、ローカルとデプロイ環境で検証手順を揃えられる。
Content Collectionsの設計で守るべき境界は明快である。Markdownは文章とFrontmatterを持ち、スキーマは入力を検証し、ページは検証済みデータだけを表示する。役割を混ぜずに保つことで、記事数が増えても条件分岐と型アサーションを増やさずに運用できる。
確認手順と失敗しやすい点
この構成では、プロジェクト直下でnvm useを実行してからnpm run buildを実行する。ビルドはMarkdownの読み込み、スキーマ検証、記事ページの静的生成までを通して確認する手順になる。
既存記事を壊さずに項目を増やすため、著者・更新日・確認日は任意項目として定義している。これらを必須に変更した場合、値を追加していない記事はビルド時に検証エラーになる。反対に、tagsを文字列一つで書くなど、配列を前提とした項目の形を変えるとページ側のmap()やjoin()と整合しない。スキーマと表示コードを同時に確認することが必要である。
公式資料と関連記事
- Astro公式: Content Collections —
glob()ローダー、スキーマ、updatedDateを含むFrontmatterの公式仕様を確認できる。 - Astroで構築するSEOに強いメタタグとOGP画像の共通コンポーネント設計 — 検証済みFrontmatterを記事ページのメタデータへ渡す実装を扱う。