Astro 5.0 Content Layer API で外部データソースを統合管理する実装ガイド【2026年5月最新】
Astro 5.0の新機能Content Layer APIを使い、複数のCMS・API・ローカルファイルを統一インターフェースで管理する実装方法を解説。型安全性とビルド最適化を実現する実践ガイド。
Astro 5.0で正式導入されたContent Layer APIは、外部CMS・REST API・GraphQL・ローカルMarkdownなど複数のコンテンツソースを統一的に扱える新しいデータ管理レイヤーです。従来のContent Collectionsがローカルファイル中心だったのに対し、Content Layer APIはヘッドレスCMSやAPIからのデータ取得を標準でサポートし、型安全性とビルド時最適化を両立します。
本記事では、Content Layer APIの基本構造から実際のCMS統合・キャッシュ戦略まで、実装に必要な全知識を解説します。
Content Layer APIとは — 従来のContent Collectionsとの違い
従来のContent Collectionsの制約
Astro 4.x系までのContent Collectionsは以下の制約がありました。
- ローカルの
src/content/ディレクトリ配下のファイルのみ対象 - 外部APIからのデータ取得は別途
getStaticPathsで実装が必要 - ビルド時に毎回APIリクエストが発生し、ビルド時間が増大
- 型定義とデータソースが分離され、型安全性の保証が困難
Content Layer APIの特徴
Content Layer APIは以下の機能を提供します。
- 統一インターフェース: ローカル・CMS・API問わず同じコードで扱える
- Loader抽象化: データソースごとにLoaderを定義し、データ取得ロジックを分離
- ビルドキャッシュ: APIレスポンスをキャッシュし、差分ビルド時に再取得を回避
- 型安全性: Zodスキーマによるバリデーションとコンテンツ型推論
graph TD
A[アプリケーションコード] --> B[Content Layer API]
B --> C[Loader: Markdown]
B --> D[Loader: Contentful]
B --> E[Loader: REST API]
B --> F[Loader: GraphQL]
C --> G[ローカルファイル]
D --> H[Contentful CMS]
E --> I[WordPress REST API]
F --> J[GraphQL Endpoint]
B --> K[型推論・バリデーション]
K --> L[Zodスキーマ]
図: Content Layer APIのアーキテクチャ。複数のLoaderが異なるデータソースからコンテンツを取得し、統一インターフェースで提供する
Content Layer APIの基本実装
コレクション定義
Content Layer APIはsrc/content/config.tsでコレクションを定義します。
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { contentfulLoader } from '@astrojs/contentful';
const blog = defineCollection({
loader: contentfulLoader({
spaceId: import.meta.env.CONTENTFUL_SPACE_ID,
accessToken: import.meta.env.CONTENTFUL_ACCESS_TOKEN,
contentType: 'blogPost',
}),
schema: z.object({
title: z.string(),
slug: z.string(),
publishedAt: z.coerce.date(),
author: z.string(),
body: z.string(),
tags: z.array(z.string()).optional(),
}),
});
export const collections = { blog };
コンテンツの取得
定義したコレクションはgetCollectionでクエリします。
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const posts = await getCollection('blog');
const sortedPosts = posts.sort((a, b) =>
b.data.publishedAt.getTime() - a.data.publishedAt.getTime()
);
---
<ul>
{sortedPosts.map(post => (
<li>
<a href={`/blog/${post.data.slug}`}>
{post.data.title}
</a>
<time>{post.data.publishedAt.toLocaleDateString('ja-JP')}</time>
</li>
))}
</ul>
型推論により、post.data.titleなどのプロパティは自動補完され、型エラーも検出されます。
カスタムLoader実装 — REST APIからのデータ取得
公式Loaderが存在しないCMSやAPIの場合、カスタムLoaderを実装できます。
Loaderインターフェース
// src/loaders/wordpressLoader.ts
import type { Loader } from 'astro/loaders';
interface WordPressPost {
id: number;
slug: string;
title: { rendered: string };
date: string;
content: { rendered: string };
}
export function wordpressLoader(siteUrl: string): Loader {
return {
name: 'wordpress-loader',
async load({ store, logger }) {
const url = `${siteUrl}/wp-json/wp/v2/posts?per_page=100`;
logger.info(`Fetching WordPress posts from ${url}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`);
}
const posts: WordPressPost[] = await response.json();
for (const post of posts) {
store.set({
id: post.slug,
data: {
title: post.title.rendered,
slug: post.slug,
publishedAt: new Date(post.date),
body: post.content.rendered,
},
});
}
logger.info(`Loaded ${posts.length} posts`);
},
};
}
カスタムLoaderの利用
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { wordpressLoader } from '../loaders/wordpressLoader';
const blog = defineCollection({
loader: wordpressLoader('https://example.com'),
schema: z.object({
title: z.string(),
slug: z.string(),
publishedAt: z.coerce.date(),
body: z.string(),
}),
});
export const collections = { blog };
キャッシュ戦略とビルド最適化
Content Layer APIはローダーが取得したデータを.astro/content-layer/にキャッシュします。
キャッシュの動作
sequenceDiagram
participant Build as ビルドプロセス
participant Cache as .astro/content-layer/
participant Loader as Loader
participant API as 外部API
Build->>Cache: キャッシュチェック
alt キャッシュが存在し有効
Cache-->>Build: キャッシュデータ返却
else キャッシュが無効または存在しない
Build->>Loader: load() 呼び出し
Loader->>API: データ取得
API-->>Loader: レスポンス
Loader->>Cache: データ保存
Cache-->>Build: データ返却
end
図: Content Layer APIのキャッシュフロー。初回ビルド時はAPIから取得し、2回目以降はキャッシュを利用
キャッシュの無効化
キャッシュを強制的にクリアするには以下を実行します。
rm -rf .astro/content-layer/
npm run build
CI/CD環境では、環境変数によるキャッシュ制御も可能です。
ASTRO_SKIP_CONTENT_CACHE=true npm run build
複数データソースの統合 — ハイブリッドコンテンツ管理
ローカルMarkdownとCMSを併用するケースでは、複数コレクションを定義します。
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
import { contentfulLoader } from '@astrojs/contentful';
const localBlog = defineCollection({
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
publishedAt: z.coerce.date(),
draft: z.boolean().optional(),
}),
});
const externalBlog = defineCollection({
loader: contentfulLoader({
spaceId: import.meta.env.CONTENTFUL_SPACE_ID,
accessToken: import.meta.env.CONTENTFUL_ACCESS_TOKEN,
contentType: 'blogPost',
}),
schema: z.object({
title: z.string(),
slug: z.string(),
publishedAt: z.coerce.date(),
}),
});
export const collections = { localBlog, externalBlog };
複数コレクションの統合表示
---
// src/pages/blog/index.astro
import { getCollection } from 'astro:content';
const [localPosts, externalPosts] = await Promise.all([
getCollection('localBlog'),
getCollection('externalBlog'),
]);
const allPosts = [...localPosts, ...externalPosts]
.filter(post => !post.data.draft)
.sort((a, b) =>
b.data.publishedAt.getTime() - a.data.publishedAt.getTime()
);
---
<ul>
{allPosts.map(post => (
<li>
<a href={`/blog/${post.data.slug || post.slug}`}>
{post.data.title}
</a>
</li>
))}
</ul>
エラーハンドリングと型安全性の強化
Zodバリデーションの実践
Zodスキーマでデータ整合性を保証します。
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
loader: wordpressLoader('https://example.com'),
schema: z.object({
title: z.string().min(1, 'タイトルは必須です'),
slug: z.string().regex(/^[a-z0-9-]+$/, 'スラッグは英数字とハイフンのみ'),
publishedAt: z.coerce.date().refine(
date => date <= new Date(),
'未来の日付は無効です'
),
tags: z.array(z.string()).max(5, 'タグは5個まで'),
}),
});
ビルド時にスキーマ違反があると、具体的なエラーメッセージが表示されます。
Content validation error at blog/invalid-post:
- "slug" must match pattern ^[a-z0-9-]+$
- "publishedAt" is invalid date
Loaderでのエラーハンドリング
export function wordpressLoader(siteUrl: string): Loader {
return {
name: 'wordpress-loader',
async load({ store, logger }) {
try {
const response = await fetch(`${siteUrl}/wp-json/wp/v2/posts`, {
headers: { 'User-Agent': 'Astro Content Layer' },
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const posts = await response.json();
for (const post of posts) {
try {
store.set({
id: post.slug,
data: {
title: post.title.rendered,
slug: post.slug,
publishedAt: new Date(post.date),
body: post.content.rendered,
},
});
} catch (err) {
logger.warn(`Skipped invalid post: ${post.slug}`, err);
}
}
logger.info(`Successfully loaded ${posts.length} posts`);
} catch (err) {
logger.error('Failed to load WordPress posts', err);
throw err;
}
},
};
}
まとめ
- Content Layer APIはAstro 5.0の新機能で、ローカル・CMS・APIを統一的に扱える
- Loader抽象化により、データソースごとの実装を分離し、型安全性を保証
- ビルドキャッシュでAPIリクエストを削減し、ビルド時間を短縮
- カスタムLoader実装でWordPress・microCMS・独自APIなど任意のソースに対応可能
- Zodスキーマでデータバリデーションを強化し、ビルド時にエラーを検出
- 複数コレクションの統合でローカルMarkdownとCMSのハイブリッド管理が可能
Content Layer APIは、従来のStatic Site Generatorの「ローカルファイル依存」という制約を解消し、ヘッドレスCMS時代に最適化されたコンテンツ管理を実現します。既存のAstroプロジェクトへの段階的な導入も可能なため、Content Collectionsからの移行も容易です。