zudo-paper

AstroのContent Collectionって何なの?

Author: Takazudo | 作成: 2026/03/12

概要

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に格納 → ページ生成時にクエリ → ビルド後破棄

ConceptRole
src/content.config.tsコレクション定義、loader、Zodスキーマ
Loaderデータ取得 — ローカルファイル用組み込み(glob())、リモート用カスタム/サードパーティ
SchemaZodバリデーション
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 } }));
}

比較表はこう。

ApproachBuild modeMulti-tag filterSEOComplexity
JSON dump + islandStaticYes要追加対応Low
SSR API endpointServerYes要追加対応Medium
Static tag pages + View TransitionsStaticNo(1タグ/ページ)GoodLow

典型的なブログならOption 1(JSONダンプ)が実用的。

Astroにサーバーサイド機能はある? Node.jsが必要?

Takazudo

Astroにサーバーサイド機能はあるの? 使うにはNode.jsのライブ環境がサーバー側に必要?

Claude

Astroにはフルのサーバーサイド機能がある。そして必ずしも従来型のNode.jsサーバーは不要。

Astroはアダプターシステムを採用していて、デプロイ先に合わせてアダプターを選ぶ。

AdapterRuntimeWhat it is
@astrojs/nodeNode.js従来型サーバー(standaloneまたはExpressミドルウェア)
@astrojs/netlifyServerless / EdgeNetlify Functions
@astrojs/vercelServerless / EdgeVercel Functions
@astrojs/cloudflareEdge WorkersCloudflare Workers/Pages
Community adaptersVariousDenoBunAWS 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、認証システム、セッション管理は提供しない。必要なら自分で組み立てる。

メンタルモデルの比較はこう。

ToolRole
Express / Fastify汎用バックエンドフレームワーク
Rails / Djangoフルスタック(DB、ORM、認証など組み込み)
Next.js / Nuxtフロントエンド中心、SSR + APIルート付き
AstroNext.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)」のほうが正確。

TermWhat people imagineWhat 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」という用語が実態以上に洗練されたものに聞こえさせている。中身はこう。

  1. ビルド時: loaderがデータ収集 → JSモジュールにシリアライズ
  2. ランタイム: サーバーレス関数がそのモジュールを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ライクなオブジェクトから読み取る薄い関数。