メインコンテンツへスキップ
#Web制作 約7分で読めます

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からの移行も容易です。

参考リンク

#Astro #Content Layer #API統合 #TypeScript #CMS
シェア