BLOG

Content Layer APIで進化したAstro×microCMSのブログ構築

Written by naganoma

INDEX

Astro v5より追加されたContent Layer APIにより、ヘッドレスCMSなどのリモートAPIから取得したデータも、Content Collectionsとして取り扱えるようになりました。

本記事では、microCMS公式チュートリアル「AstroとmicroCMSでつくるブログサイト」と比較して、microCMSの記事取得にContent Layer APIを活用するメリットと、具体的な導入方法についてご紹介します。

Content Layer APIとは

Content Layer APIはAstro v5から登場した機能で、Content Collections APIの進化版に当たります。

Content Collectionsとは、簡単に言うとローカルファイル(.mdx.jsonなど)で作成した記事データを使って動的にHTMLページを生成できる機能のことです。
(※詳しくは、弊社記事の「AstroのContent Collectionsでブログをつくる」でもご紹介しています。)

Content Collectionsはデータの参照元が「特定のディレクトリ内のローカルファイルのみ」という制約がありましたが、Content Layer APIの登場により、「Loader」という機能を使ってデータの参照元を柔軟に指定できるようになりました。

src/content/config.ts
// v4までの記法
import { z, defineCollection } from 'astro:content';

const blogLegacyCollection = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
  }),
});

export const collections = {
  blog_legacy: blogLegacyCollection,
};
src/content.config.ts
// v5以降の記法
import { z, defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  // 参照元を指定
  loader: glob({ pattern: ['*.mdx'], base: 'src/content/blog' }),
  schema: z.object({
    title: z.string(),
  }),
});

export const collections = { blog };

Content Layer APIを導入するメリット

1. キャッシュ戦略

Astroは「More HTML, Less JavaScript」をコンセプトとしており、「静的なHTMLファイルを事前に⽣成することで、ビルド後のJavaScriptを削減する」という設計思想がベースとなっています。(=SSG構成)

SSG構成を設計する上でまず課題となるのが、コンテンツデータのキャッシュ戦略です。
以下に例を挙げます。

src/library/microcms.ts
import { createClient } from "microcms-js-sdk";

// microcmsクライアントオブジェクトの作成
export const client = createClient({ 
  serviceDomain: 'YOUR_DOMAIN',
  apiKey: 'YOUR_API_KEY',
});
src/pages/index.astro
---
// コンテンツデータの出力
import { client } from "../library/microcms";
const response = await client.getAllContents({
  endpoint: 'blog',
})
---

<ul>
  {
    response.contents.map((content) => (
      <li>
        <a href={content.id}>{content.title}</a>
      </li>
     ))
    }
</ul>

microCMSのコンテンツAPIから記事を取得し、記事一覧を表示するシンプルな例です。
一見問題ないように見えますが、ここでネックとなるのが「記事データの取得処理をファイル単位で都度実行している」という点です。

例えば、上記と同じ記事データを/pickupページでも表示する場合、TOPページと同じ処理にも関わらず、TOPページとは別のワークフローで実行されてしまいます。
このような書き方をしてしまうと余分な処理が増え、npm run buildの完了に時間がかかってしまいます。

よって、SSG構成では記事データをキャッシュ化するロジックをビルドプロセス内に組み込むことが通例ですが、Content Layer APIを使用することで、このロジックを簡単に実装できるようになりました。

src/content.config.ts
import { defineCollection, z } from 'astro:content'
import { client } from './library/microcms'

const blog = defineCollection({
  loader: async () => {
    const response = await client.getAllContents({
      endpoint: 'blog',
    })
    return response.map((content) => ({
      id: content.id,
      ...content,
    }))
  },
  schema: z.object({
    title: z.string(),
    body: z.string(),
  }),
})

export const collections = { blog };
src/pages/index.astro
---
import { getCollection } from 'astro:content'
const response = await getCollection('blog')
---

<ul>
  {
    response.contents.map((content) => (
      <li>
        <a href={content.id}>{content.data.title}</a>
      </li>
     ))
    }
</ul>

Content Collectionsは登録したデータを「Data Store」と呼ばれる場所に保存し、以降はこのData Storeからデータを参照することでキャッシュ戦略を実現しています。
よって、microCMSから取得した記事データをContent Collectionsに登録し、以降は登録したCollectionsデータを参照することで、この問題を解決することができます。

https://astro.build/blog/content-layer-deep-dive/

※Content Layer APIが登場する以前はキャッシュ変数を自作する必要がありました。
詳しくはmicroCMS公式の「microCMS + AstroのJamstack構成でキャッシュを活用し、ビルド時間を90%以上短縮した方法」をご参照ください。

2. 型安全性

Content Collectionsは元より型安全性に優れた機能で、リモートAPIから取得したデータも同様に各々のフィールドに型付けすることができます。

src/content.config.ts
import { defineCollection, z } from 'astro:content'
import { client } from './library/microcms'

const blog = defineCollection({
  loader: async () => {
    const response = await client.getAllContents({
      endpoint: 'blog',
    })
    return response.map((content) => ({
      id: content.id,
      ...content,
    }))
  },
  // 型設定
  schema: z.object({
    title: z.string(),
    body: z.string(),
  }),
})

export const collections = { blog };

Collectionsデータに登録する際にschemaプロパティに入力フィールドの型設定を行います。
この設定によってnpm run devnpm run build実行時にバリデーションチェックが走るようになり、誤った記法や存在しない入力フィールドなどがあった場合にエラーを検知することができます。

また、CollectionEntry型経由でコンテンツデータの型を参照できるため、以下のようなコンポーネントを実装する際も安全に開発することができます。

src/components/BlogList.astro
---
// 一覧表示用コンポーネント
import type { CollectionEntry } from 'astro:content'

interface Props {
  response: CollectionEntry<'blog'>
}

const { response } = Astro.props
---

<ul>
  {
    response.contents.map((content) => (
      <li>
        <a href={content.id}>{content.data.title}</a>
      </li>
     ))
    }
</ul>

Content Layer APIで記事詳細・一覧ページを作成する

では、実際にContent Layer APIを使ってmicroCMSから取得したブログ記事の詳細ページと一覧ページを作成してみましょう。

microCMSの設定

まずはmicroCMSの管理画面上でブログ(blog)というAPIを作成します。

APIの型の選択では「リスト形式」を選択します。

入力フィールドを設定します。
今回の例では「タイトル」、「サムネイル」、「本文」の簡易的な構成としました。

あとは「作成」ボタンを押してmicroCMS側の設定は完了です。
テスト表示用にダミー記事を作成しておいてください。

Astroの設定

1. インストール〜パッケージ準備

まずはAstroをインストールします(執筆時点でv5.14.1)
パッケージマネージャーはnpmを使用します。(※Node.jsはインストールされているものとします)

npm create astro@latest

セットアップの質問に回答します。

* `Where should we create your new project?` (プロジェクト作成場所)
→ プロジェクトのディレクトリを指定

* `How would you like to start your new project?` (テンプレート選択)
→ `A basic, helpful starter project` を選択

* `Install dependencies?` (依存関係のインストール)
→ `Yes` を選択

* `Initialize a new git repository?` (Git初期化)
→ 必要であれば `Yes` を選択

セットアップが完了したら、cd {プロジェクトを作成したディレクトリ}でプロジェクトディレクトリに移動し、microcms-js-sdkをインストールしておきます。

npm install microcms-js-sdk

2. microCMSの記事データを取得

microcms-js-sdkでmicroCMSクライアントオブジェクトを作成します。

src/library/microcms.ts
import { createClient } from "microcms-js-sdk";

// microcmsクライアントオブジェクトの作成
export const client = createClient({ 
  serviceDomain: 'YOUR_DOMAIN',
  apiKey: 'YOUR_API_KEY',
});

serviceDomainhttps://{service-domain}.microcms.io/の「service-domain」の部分、
apiKeyはmicroCMS管理画面の「APIプレビュー」より確認できます。

次にAstroのContent Layer APIを使ってContent Collectionsを作成します。

src/content.config.ts
import { defineCollection, z } from 'astro:content'
import { client } from './library/microcms'

const blog = defineCollection({
 // microCMSのコンテンツAPI(blog)にアクセスしてCollectionsに登録
  loader: async () => {
    const response = await client.getAllContents({
      endpoint: 'blog',
      queries: {
        orders: '-publishedAt',
      }
    })
    return response.map((content) => ({
    // コンテンツID(記事のslug)の値
      id: content.id,
      // 入力フィールドの値(title、thumbnailなど)
      ...content,
    }))
  },
 // 入力フィールドの型設定
  schema: z.object({
    title: z.string(),
    thumbnail: z.object({
      url: z.string(),
    height: z.number(),
      width: z.number(),
    }),
    body: z.string(),
  }),
})

export const collections = { blog };

3. 記事詳細・一覧ページの作成

以降は登録したCollectionsデータを使って記事データを出力します。
ブログの記事詳細ページはAstroの動的ルーティング機能を使用して動的に生成します。

src/pages/blog/[...slug].astro
---
import { getCollection } from 'astro:content'

export const getStaticPaths = async () => {
  const response = await getCollection('blog')
}

return response.map((content, index) => ({
  params: {
    slug: content.id,
  },
  props: {
    entry: content
  }
})

const { entry } = Astro.props;
---

{/* タイトルフィールド */}
<h1>{entry.data.title}</h1>

{/* サムネイルフィールド */}
<img src={entry.data.thumbnail.url} width={entry.data.thumbnail.width} height={entry.data.thumbnail.height} alt="" />

{/* 本文フィールド */}
<div set:html={entry.data.body}></div>

ブログの記事一覧ページはAstroのPagination機能を使用して動的に生成します。

src/pages/blog/[...page].astro
---
import type { GetStaticPathsOptions } from 'astro'
import { getCollection } from 'astro:content'

export const getStaticPaths = async ({ paginate }: GetStaticPathsOptions) => {
  const response = await getCollection('blog')
  return paginate(response, {
    // 1ページあたりの表示数
    pageSize: 12,
  })
}

const { page } = Astro.props
---

<div>
  {
    page.data.map((entry) => (
      <article>
        <a href={`/blog/${entry.id}/`}>
          <img src={entry.data.thumbnail.url} width={entry.data.thumbnail.width} height={entry.data.thumbnail.height} alt="" />
          <h2>{entry.data.title}</h2>
        </a>
      </article>
    ))
  }
</div>

簡易的ですが以上で完成です。
ローカルサーバー上(http://localhost:{PORT}/)で記事が表示されていることを確認してみてください。

EvoLab.での実験結果

本サイトでは、2025年6月からContent Layer APIを使用して記事ページを生成しています。
導入前後でビルド時間がどれくらい変わったか、実際に比較してみました。
(※ホスティングはCloudflare Pagesを使用。直近3デプロイのビルドログより確認。)

Content Layer API導入前
(キャッシュ変数なし)

Content Layer API導入前
(キャッシュ変数あり)

Content Layer API導入後

110秒〜125秒

80秒〜90秒

55秒〜60秒

導入前後で記事数やフレームワークのバージョンなども変わっているため、完璧な対照実験とは言い切れませんが、キャッシュ変数なしの環境と比べて約50%ほどビルド時間を短縮できました。

また、Content Layer APIの導入前に一度キャッシュ変数を自作してビルド時間の短縮を図っていましたが、ロジックに粗や見落としがあった可能性が高く、
Content Layer APIを導入することで、その問題もカバーされて上手く最適化されたのではないかと考えています。

参考文献

Astro Content Loader API | Docs

Content collections | Docs

Content Layer: A Deep Dive | Astro

microcmsio/microcms-js-sdk: microCMS JavaScript SDK.