概要
AstroのContent Collectionについて調べた。ビルド時の一時的なDB的仕組みなのか、サーバーサイドでどう動くのか、サーバーレス環境でのキャッシュの実態は何なのか。Claude Codeに聞きながら掘り下げていった。以下はそのやりとりをまとめたもの。
Content Collectionとは? ビルド時の一時的なDBみたいなもの?
Takazudo
AstroのContent Collectionって何? ビルドプロセスでだけ存在する一時的なDB的システムなの?
Claude
Content Collectionは、構造的に似たデータ(ブログ記事、商品情報など)を型安全に整理・バリデーション・クエリする仕組み。Zodでスキーマを定義し、getCollection('blog')やgetEntry('blog', 'my-post')でクエリする。
「ビルド時の一時DBか?」という理解はかなり正確なメンタルモデル。具体的にはこうなっている。
- ビルド時にloaderがデータを取得(ローカルMarkdown/JSONファイル、CMS、DB、APIなど)し、内部のdata storeに格納
- ページからはこのストアを
getCollection()/getEntry()でクエリ - data storeはビルド間でキャッシュされ、インクリメンタルリビルドで全件再取得しなくてよい
- ビルド後(静的サイトの場合)、data storeはランタイムには存在しない。クエリ済みデータはHTMLに焼き込まれている
つまり: loaderがデータ取得 → ビルド時data storeに格納 → ページ生成時にクエリ → ビルド後破棄
| Concept | Role |
|---|---|
src/content.config.ts | コレクション定義、loader、Zodスキーマ |
| Loader | データ取得 — ローカルファイル用組み込み(glob())、リモート用カスタム/サードパーティ |
| Schema | Zodバリデーション |
getCollection() / getEntry() | astro:contentからインポートするクエリAPI |
| Data Store | ビルド時にloaderが投入する内部キャッシュ |
Experimental: Live Collectionsという実験的機能もあり、ビルド時ではなくランタイムでデータ取得する。
ブログのタグで動的検索をしたい場合、Live Collectionsが使える?
Takazudo
ブログにタグがある場合、SPAのような動的タグ検索機能を実装したい。新しいLive Collections機能が使える? それとも大きなJSONにダンプして再利用する必要がある?
Claude
Live CollectionsはSSR(サーバーサイド)のランタイム機能であり、クライアントサイドの機能ではない。通常のContent CollectionもLive Collectionsもブラウザでは動かない。
SPA的な動的タグフィルタリングにはブラウザ側にデータが必要。実用的な選択肢は3つある。
Option 1: JSONにダンプ(最もシンプル、静的ビルドで動作)
ビルド時にJSONファイルを生成し、クライアントサイドのコンポーネント(React/Vue/Svelteアイランド、またはバニラJS)で消費する。
// src/pages/api/posts.json.ts (static endpoint)
import { getCollection } from "astro:content";
export async function GET() {
const posts = await getCollection("blog");
const payload = posts.map((p) => ({
id: p.id,
title: p.data.title,
tags: p.data.tags,
url: `/blog/${p.id}`,
}));
return new Response(JSON.stringify(payload));
}
アイランドコンポーネントが/api/posts.jsonをフェッチしてクライアントサイドでフィルタリングする。
Option 2: SSR APIエンドポイント(すでにSSRモードの場合)
output: 'server'または'hybrid'モードなら、サーバーサイドでコレクションをクエリする動的APIエンドポイントを作れる。
// src/pages/api/posts-by-tag/[tag].json.ts
import { getCollection } from "astro:content";
export async function GET({ params }) {
const posts = await getCollection("blog", ({ data }) =>
data.tags.includes(params.tag),
);
return new Response(JSON.stringify(posts.map(/* ... */)));
}
Option 3: 静的タグページ + View Transitions(JSなし)
タグごとに静的ページを生成し、AstroのView TransitionsでSPA風ナビゲーションにする。
// src/pages/blog/tag/[tag].astro
export async function getStaticPaths() {
const posts = await getCollection('blog');
const tags = [...new Set(posts.flatMap(p => p.data.tags))];
return tags.map(tag => ({ params: { tag } }));
}
比較表はこう。
| Approach | Build mode | Multi-tag filter | SEO | Complexity |
|---|---|---|---|---|
| JSON dump + island | Static | Yes | 要追加対応 | Low |
| SSR API endpoint | Server | Yes | 要追加対応 | Medium |
| Static tag pages + View Transitions | Static | No(1タグ/ページ) | Good | Low |
典型的なブログならOption 1(JSONダンプ)が実用的。
Astroにサーバーサイド機能はある? Node.jsが必要?
Takazudo
Astroにサーバーサイド機能はあるの? 使うにはNode.jsのライブ環境がサーバー側に必要?
Claude
Astroにはフルのサーバーサイド機能がある。そして必ずしも従来型のNode.jsサーバーは不要。
Astroはアダプターシステムを採用していて、デプロイ先に合わせてアダプターを選ぶ。
| Adapter | Runtime | What it is |
|---|---|---|
@astrojs/node | Node.js | 従来型サーバー(standaloneまたはExpressミドルウェア) |
@astrojs/netlify | Serverless / Edge | Netlify Functions |
@astrojs/vercel | Serverless / Edge | Vercel Functions |
@astrojs/cloudflare | Edge Workers | Cloudflare Workers/Pages |
| Community adapters | Various | Deno、Bun、AWS Lambdaなど |
有効化はこう。
// astro.config.mjs
import { defineConfig } from "astro/config";
import netlify from "@astrojs/netlify";
export default defineConfig({
output: "server",
adapter: netlify(),
});
outputには3つのモードがある。
'static'(デフォルト)— 完全静的サイト、サーバー不要'server'— 全ページをオンデマンドレンダリング(SSR)'hybrid'— デフォルト静的、個別ページをSSRにオプトイン
AstroはバックエンドにDBを作る? REST APIのように動く?
Takazudo
そういった環境で、AstroはバックエンドにDBを作るの? 一般的なREST APIのように振る舞う?
Claude
AstroはバックエンドにDBを作らないし管理もしない。SSRモードのAstroは単なるリクエストハンドラ — HTTPリクエストを受けてレンダリング済みHTMLまたはJSONレスポンスを返す。
データソースは自分で用意する。Astroは何であるかを気にしない。
// src/pages/api/posts-by-tag/[tag].json.ts
import { db } from "../../lib/my-database"; // Postgres, SQLiteなど
import { cms } from "../../lib/my-cms"; // Contentful, Sanityなど
import { getCollection } from "astro:content"; // ローカルMarkdownファイル
export async function GET({ params }) {
// Option A: 自前のDB
const postsA = await db.query("SELECT * FROM posts WHERE tag = $1", [
params.tag,
]);
// Option B: ヘッドレスCMS API
const postsB = await cms.getPostsByTag(params.tag);
// Option C: Content Collections(ビルド時に構築、ランタイムで読み取り)
const postsC = await getCollection("blog", ({ data }) =>
data.tags.includes(params.tag),
);
return new Response(JSON.stringify(postsC));
}
REST API的なエンドポイントは定義できる。
src/pages/api/posts.json.ts → GET /api/posts.json
src/pages/api/posts/[id].json.ts → GET /api/posts/123.json
ただしAstroはバックエンドフレームワークではない。DB管理/マイグレーション、ORM、認証システム、セッション管理は提供しない。必要なら自分で組み立てる。
メンタルモデルの比較はこう。
| Tool | Role |
|---|---|
| Express / Fastify | 汎用バックエンドフレームワーク |
| Rails / Django | フルスタック(DB、ORM、認証など組み込み) |
| Next.js / Nuxt | フロントエンド中心、SSR + APIルート付き |
| Astro | Next.js/Nuxtと同じカテゴリ — フロントエンド中心、オプションでSSR + APIルート |
Content Collectionsをブログのローカルファイルに使う場合、SSRモードでもビルド時data storeからの読み取りになる。ライブDBではない。
結局、キャッシュ済みデータをREST APIっぽく返してるだけ?
Takazudo
つまりAstroはキャッシュ済みデータを、あたかもREST APIを使っているかのように返しているだけ?
Claude
その通り。Content CollectionsをSSRモードで使う場合はこうなる。
astro build
→ loaderが実行、Markdown/データを全件取得
→ キャッシュ済みdata storeとして格納(ビルド出力にバンドル)
リクエスト着信
→ getCollection('blog')がそのキャッシュから読む
→ レスポンスとして返す
クライアント側から見ればREST APIに見えるが、裏側ではビルド済みキャッシュから読んでいるだけ。ライブDB無し、リアルタイムクエリ無し。
だからローカルMarkdownファイルのブログでは、以下の2つにほぼ実用的な差がない。
- Static: ビルド時にJSONダンプ、クライアントが
.jsonファイルを読む - SSR: クライアントがAPIエンドポイントにアクセス、Astroが同じビルド時キャッシュを読んでJSONを返す
両方とも同じ「リビルドするまで古いデータ」を配信する。SSRは仲介者(サーバーレス関数)を追加するだけで、この場合メリットがない。
Content Collectionはビルドのたびに再生成される? プログラマティックAPIでも?
Takazudo
Content Collectionはビルドを実行するたびに再生成される? AstroにはオンデマンドでbuildできるNode.js APIがある。このビルドトリガーでContent Collectionは再生成される?
Claude
毎回のastro build(プログラマティックなbuild()含む)で全コレクションloaderが再実行され、data storeが再生成される。
ビルドサイクルはこう。
astro build (or programmatic build())
├── 1. 全コレクションloaderを実行
│ └── Markdown取得、API呼び出しなど
│ └── data store(KVストア)に投入
├── 2. そのdata storeを使って全ページをレンダリング
└── 3. dist/に出力
プログラマティックAPIも全く同じパイプラインをトリガーする。
import { build } from "astro";
// astro buildと同じことをする
// Content Collectionsは完全に再生成される
await build({ root: "./my-project" });
ただしインクリメンタルキャッシュがある。Loaderはmeta経由でビルド間でメタデータを永続化できる。
load: async ({ store, meta }) => {
const lastModified = meta.get("lastModified");
// 前回ビルド以降の変更分だけ取得
const newItems = await fetchSince(lastModified);
for (const item of newItems) {
store.set({ id: item.id, data: item });
}
meta.set("lastModified", new Date().toISOString());
};
loaderは毎ビルドで実行されるが、よく書かれたloaderは変更のないデータの再取得をスキップできる。組み込みのglob() loaderもこれをやっている。
ただしbuild()はContent Collectionだけでなくサイト全体のフルリビルド。「コンテンツだけリフレッシュ」する仕組みではない。
オンデマンドのコンテンツ更新には以下のような手段がある。
- Webhookトリガーのリビルド(Netlify/Vercelがネイティブ対応 — CMS公開 → リビルドトリガー)
- 実験的Live Collections(リクエストごとにフレッシュなデータ取得、リビルド不要)
- 自前のAPIエンドポイントで外部データソースに直接クエリ(Content Collections回避)
CloudflareやNetlifyではデータはどうキャッシュされている?
Takazudo
Cloudflare、Netlifyのようなバックエンドではデータはどのようにキャッシュされている?
Claude
Content Collectionのdata storeはビルド出力に静的アセットとしてバンドルされる — サーバーコードと一緒に出荷されるシリアライズ済みファイル。DBプロセスは別途立っていない。
astro build
└── dist/
├── server/
│ ├── entry.mjs ← サーバーコード(サーバーレス関数)
│ └── chunks/
│ └── _astro_data-store_*.mjs ← コレクションデータ、インライン化
└── client/
└── ... ← 静的アセット
Cloudflare Workers、Netlify Functions、Vercel Serverless上では以下のようになっている。
- データはデプロイされた関数コードに埋め込まれている(またはバンドルと一緒に出荷)
getCollection()は本質的にメモリ上のJavaScriptオブジェクトからの読み取り- ファイルI/O無し、DB接続無し、外部キャッシュ無し
- 変数にアクセスするのと同程度の速度
だから「キャッシュ」というより「焼き込まれている(baked in)」のほうが正確。
| Term | What people imagine | What actually happens |
|---|---|---|
| ”Cache” | TTLベースのストレージレイヤー | いいえ — バンドル内の静的データ |
| ”Data store” | SQLiteやKVストア | いいえ — シリアライズ済みJSオブジェクト |
| ”Rebuild to refresh” | DBへの再クエリ | loaderの再実行、全体の再バンドル |
Content Collectionsはサーバーレスプラットフォーム上でインフラオーバーヘッドがゼロ。DBのプロビジョニング不要、キャッシュの設定不要、永続化レイヤーの心配不要。トレードオフはデータがビルド時に固定されること。更新にはリビルド&再デプロイが必要。
結局、薄いLambdaみたいな関数がJSONっぽいものから必要なデータを取り出してるだけ?
Takazudo
技術的に言えば、薄いLambdaみたいなサーバーサイド関数が、JSONっぽいものから必要なデータをピックアップしてるだけってこと?
Claude
まさにその通り。正確にそれ。
クライアントリクエスト
→ サーバーレス関数が起動
→ バンドル内のJSオブジェクトから読む
→ HTMLまたはJSONを返す
→ 終了
「Content Collection」「data store」という用語が実態以上に洗練されたものに聞こえさせている。中身はこう。
- ビルド時: loaderがデータ収集 → JSモジュールにシリアライズ
- ランタイム: サーバーレス関数がそのモジュールをimport → filter/mapで処理 → レスポンス
概念的にはこう手で書いたのと同じ。
const posts = [
{ id: "hello", data: { title: "Hello", tags: ["astro"] } },
{ id: "world", data: { title: "World", tags: ["react"] } },
];
export function getCollection(name, filter) {
return filter ? posts.filter(filter) : posts;
}
それだけ。マジック無し。バンドル済みのJSONライクなオブジェクトから読み取る薄い関数。