【Astro】microCMS の API と連携してブログ記事を表示できるようにする手順（SDK 利用ver.）

## 概要
Astro で構築した Web サイトで、外部 API から取得したデータを活用する一例として、ブログ記事での利用方法を記載します。

イメージは、CMS やヘッドレス CMS の API から、タイトルや記事本文や画像の URL を取得し、それを利用して画面に表示する処理の実装です。

CMS 側がサイト構築に利用できる専用の SDK を用意している場合、これを使うかどうかで書き方が変わるのですが、
今回は、紹介するヘッドレス CMS の SDK を使用した方法を記載します。

SDK を使用しない方法も別記事にまとめていますので、特定の CMS に依存せずにピュアな JavaScript 処理のみで実装する方法を知りたい場合は、下記をご覧ください。

https://qiita.com/to3izo/items/785b69825db17818389a

### 前提
- Astro の基本的な書き方を知っている前提として記載していきます
- CMS 側の作業手順は説明しません。あらかじめ表示用の記事が２〜３つくられている前提で進めます
  - API のレスポンス形式も CMS 側のサンプルを使用しますので、適宜読み替えてください
- 構築したブログをデプロイする手順（レンタルサーバなり Firebase なり Vercel なりにアップロードする手順）も説明しません

また、今回利用する CMS は **「[microCMS](https://microcms.io/)」** という日本産のヘッドレス CMS です。microCMS は、API 利用が簡単にできる様に JavaScript 用の SDK （**[microcms-js-sdk](https://github.com/microcmsio/microcms-js-sdk)**）を用意されており、今回はそれを使用します。

microCMS には「API プレビュー」機能があり、どのようなレスポンスが返るか確認できる機能があります。

今回 API で取得する、記事１つ分のレスポンスデータは、以下です。

```json:ブログ１記事のデータ
{
    "id": "b49k0v7g6",
    "createdAt": "2024-02-10T14:03:15.824Z",
    "updatedAt": "2024-02-10T14:03:15.824Z",
    "publishedAt": "2024-02-10T14:03:15.824Z",
    "revisedAt": "2024-02-10T14:03:15.824Z",
    "title": "（サンプル）microCMS＋Astro でブログサイトを構築する",
    "content": "<p>最近話題の「ヘッドレスCMS」を使ってブログを構築してみようと思います。</p><p>今回は、国産ヘッドレスCMSの「microCMS」と、静的サイトジェネレータの「Astro」で簡単なブログサイトを作ってみました。</p>",
    "eyecatch": {
        "url": "https://images.microcms-assets.io/assets/54e262a32b7048ccbc8dfcd1e4ff30ad/409b8ff8a331415f8251601ab5587685/blog-template.png",
        "height": 630,
        "width": 1200
    },
    "category": {
        "id": "atelo585eo88",
        "createdAt": "2024-02-10T07:49:58.078Z",
        "updatedAt": "2024-02-10T07:49:58.078Z",
        "publishedAt": "2024-02-10T07:49:58.078Z",
        "revisedAt": "2024-02-10T07:49:58.078Z",
        "name": "テクノロジー"
    }
}
```

また、上記の他に、HTML のタグたくさん使った、画像（img タグ）も含まれた長めの記事を用意しました。そちらも表示できるか確認していきます。

また、記事一覧取得時の API のレスポンスは以下です。
```json-doc:ブログ記事一覧のデータ
{
  "contents": [
    {
      // 上記の１記事分と同じデータ内容
      "id": "b49k0v7g6",
        :
    },
    {
      // 上記の１記事分と同じデータ内容
      "id": "rytmup5w4ntx",
        :
    }
  ],
"totalCount": 2,
"offset": 0,
"limit": 10
}
```

## 手順
「記事一覧」画面と「記事詳細」画面を構築し、それぞれで一覧取得用の API と詳細取得用の API を使う様に実装していきます。

### SDK をインストール
microCMS が提供している SDK をインストールしておきます。
```bash
npm install microcms-js-sdk
```

`npm` ではなく `yarn` を使っている場合、以下のコマンドです。
```bash
yarn add microcms-js-sdk
```

一応、CDN での配布もされていますので、CDN の場合はヘッダに含めましょう。
```html
<script src="https://unpkg.com/microcms-js-sdk@latest/dist/umd/microcms-js-sdk.js"></script>
```

### 環境変数の設定
まず、環境変数を設定します。`.package.json` がある階層に `.env` がファイルを用意し、以下の様に値を設定しておきます。

```bash:.env
MICROCMS_SERVICE_DOMAIN=<YOUR_SERVICE_DOMAIN>
MICROCMS_API_KEY=<API_KEY>
```

`<YOUR_SERVICE_DOMAIN>` には、API の URL `https://xxxxx.microcms.io` の `xxxxx` 部分をセットします。
`<API_KEY>` には、管理画面から確認できる API キーをセットしましょう。

### 画面の作成
次に、一覧画面と詳細画面となるページを用意します。

```
src/
  └ pages/
     └ articles
        ├ [...slug].astro
        └ index.astro
```

「記事」を意味する **「articles」** ディレクトリを作成し、一覧画面として `index.astro`、記事ID毎に表示される詳細画面として `[...slug].astro` を用意しました。詳細画面は、記事 ID 毎の動的ルーティングなので、`[変数].astro` としました。

中身は、とりあえず Astro で画面が表示できる最低限の画面を書きます。


**一覧画面**
```tsx:src/pages/articles/index.astro
---
import "../../styles/global.css";
---
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="description" content="Astro description" />
  <meta name="viewport" content="width=device-width" />
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  <meta name="generator" content={Astro.generator} />
  <title>Demo Blog | 一覧</title>
</head>
<body>
  <main>
    <div>
      一覧画面
    </div>
  </main>
</body>
</html>
```

私は、pages ディレクトリと同じ階層に `styles/global.css` を用意して適当な css を import で読み込んでますが、このファイルで `<style>` タグで書いたりすれば不要です。

**詳細画面**
```tsx:src/pages/articles/[...slug].astro
---
import "../../styles/global.css";

export function getStaticPaths() {
  return [
    { params: { slug: 1 } },
    { params: { slug: 2 } },
    { params: { slug: 3 } },
  ];
}
---
<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="description" content="Astro description" />
  <meta name="viewport" content="width=device-width" />
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
  <meta name="generator" content={Astro.generator} />
  <title>Demo Blog | 詳細</title>
</head>
<body>
  <main>
    <div>
      詳細画面
    </div>
  </main>
</body>
</html>
```

動的ルーディングを **静的（SSG）モード** で行うので、`getStaticPaths()` メソッドを使って、画面遷移ページを適当に定義しておきます。

Astro では、基本的に静的なサイトとして生成するのですが、`getStaticPaths()` を使うことで全てのページをビルド時に生成しておくことができます。

`params` の中にオブジェクトとして、変数を利用した詳細画面用のファイル名（`[...slug]` 部分）に値を合わせる必要があります。

実際にブラウザで表示確認してみます。

**一覧画面** `http://localhost:4321/articles`

👉 （すみません、画像貼り忘れました💦）


**詳細画面** `http://localhost:4321/articles/1`

![demo_detail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/787582/04f3b77a-3ff4-f176-b03d-0b51658f3437.png)

無事に表示ができました。
尚、定義していないルーティングページ（例： `/articles/4` とか）だと、静的ページが生成されていないため、"404"（ページが見つからない）エラー画面が表示されます。

### API 実行処理を作る
`libs` というディレクトリを `src` 直下に作成して、`microcms.ts` というファイルを作り、API を実行するライブラリとして処理を実装します。尚、今回は、TypeScript で書きます。
（名前は `libs` でなくても `library` でも `service` でもなんでも大丈夫です）

ここに、環境変数から値を取得して API に利用する値を定義します。

```ts:src/libs/microcms.ts
import { createClient } from "microcms-js-sdk";
import type { MicroCMSQueries } from "microcms-js-sdk";

// 環境変数のチェック・呼び出し
if (
  import.meta.env["MICROCMS_SERVICE_DOMAIN"] === undefined ||
  import.meta.env["MICROCMS_API_KEY"] === undefined
) {
  throw new Error(
    "Please set environment variables: MICROCMS_SERVICE_DOMAIN and MICROCMS_API_KEY"
  );
}

export const client = createClient({
  serviceDomain: import.meta.env["MICROCMS_SERVICE_DOMAIN"],
  apiKey: import.meta.env["MICROCMS_API_KEY"],
});


// 型定義
export type Article = {
  id: string;
  createdAt: Date;
  updatedAt: Date;
  publishedAt: Date;
  revisedAt: Date;
  title: string;
  content: string;
  eyecatch: {
    url: string;
    height: number;
    width: number;
  }
  category: {
    id: string;
    createdAt: Date;
    updatedAt: Date;
    publishedAt: Date;
    revisedAt: Date;
    name: string;
  }
}

export type ArticleResponse = {
  totalCount: number;
  offset: number;
  limit: number;
  contents: Article[];
}

// API 呼び出し
const endpointArticles = "articles";

// CMS から API で記事データを取得する (一覧)
export const getArticles = async (queries?: MicroCMSQueries) => {
  return await client.get<ArticleResponse>({
    endpoint: endpointArticles,
    queries, // queries: {limit: 10, fields: "id"}, などと書いて, 引数を省略しても良い
  });
}

// CMS から API で記事データを取得する (詳細)
export const getArticleDetail = async (
  contentId: string,
  queries?: MicroCMSQueries
) => {
  return await client.getListDetail<Article>({
    endpoint: endpointArticles,
    contentId,
    queries,
  });
}
```

Astro は中で「Vite」が動いてるのですが、Vite では、環境変数は `process.env` の代わりに、ES2020 で追加された `import.meta` 機能を使用した `import.meta.env` を使用します。

また、型定義は同じ場所に記載してますが、お好みで別ファイルに分けても良いと思います。

API 実行時に引数を `queries?: MicroCMSQueries` とクエリ指定できる様にしておくことで、画面側から利用する場合に「記事の ID だけ欲しい」「タイトルとサムネイルの URL だけ欲しい」みたいなことが出来る様になります。

#### get メソッドには複数種類ある
一覧取得に使えるメソッドは実は色々あります。
上記では `get<T>()` を使用していますが、これは汎用的な書き方です。今回の様に「配列を含んだレスポンスデータである」ということが明確である場合は、`getList<T>()` も使用できます。

**`getList<T>()` を使用する場合の書き方**
```ts:src/libs/microcms.ts
// CMS から API で記事データを取得する (一覧)
export const getArticles = async (queries?: MicroCMSQueries) => {
  return await client.getList<Article>({
    endpoint: endpointArticles,
    queries,
  });
}
```

#### get メソッド と getList メソッドの違い
両者のメソッドの違いをざっくりまとめると以下の２点です。

**❶ 指定する型が変わる**
`get<T>()` の場合は、`<T>` に「API レスポンスの型」を指定します（今回なら `type ArticleResponse`）。

`getList<T>()` の場合は、`<T>` に「API レスポンスの内、配列となるデータの型」を指定します（今回なら `type Article`）。
こちらを使用した場合、API レスポンスとしての返却データの型は自動的に `MicroCMSListResponse<T>` 型となります（今回なら `MicroCMSListResponse<Article>`）。

つまり、`getList<T>()` を利用すると API レスポンスの型は自動できまるので、`type ArticleResponse` の定義は記載不要になります。

**❷ レスポンスの型も変わる**
また、結果的に、取得できる配列のデータ型にも少し違いが発生します。

`get<T>()` の場合は、最終的に取得できる配列データは `type ArticleResponse` の `contents` で指定した通り `Article[]` 型となります。

しかし、`getList<T>()` の場合は、最終的に取得できる配列データ `contents` は自動的に `(T & MicroCMSContentId & MicroCMSDate)[]` 型となります。
つまり、今回の例で言うと `(Article & MicroCMSContentId & MicroCMSDate)[]` 型になります（型が拡張されて、コンテンツ ID なども取得できる感じ）。

**どちらを採用するかはお好みで**
どちらを使うかはお好みですが、利用したいデータが配列であると決まっている場合は `getList<T>()` が使いやすいかと思います。

### 画面で API 処理を呼ぶ
.astro ファイルの `---` 領域の中にサーバサイド処理を書けるので、そこに先ほど実装した記事取得処理を追加します。
また、取得した記事をループ処理で画面に表示する処理も記載します。

本来は、適切なタグを選んで、スタイル整えて `<ul>` や `<li>` で並べるべきですが、今回はわかりやすく適当に `<div>` タグで羅列します。

**一覧画面**
```diff_tsx:src/pages/articles/index.astro
 ---
 import "../../styles/global.css";
+// 記事一覧を取得
+import { getArticles } from "../../libs/microcms";

+// レスポンスは、記事の ID, タイトル, サムネイル画像 のみを取得する
+const apiResponse = await getArticles({ fields: ["id", "title", "eyecatch"] });
+const articles = apiResponse.contents;
 ---
 <!doctype html>
 <html lang="ja">
   <meta charset="UTF-8" />
   <meta name="description" content="Astro description" />
   <meta name="viewport" content="width=device-width" />
   <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
     <meta name="generator" content={Astro.generator} />
     <title>Demo Blog | 一覧</title>
   </head>
   <body>
     <main>
-      一覧画面

+      {
+        // 記事一覧をループして表示
+        articles.map((article) => (
+          <div>
+            <a href={`/articles/${article.id}/`}>
+              <img
+                width={article.eyecatch.width}
+                height={article.eyecatch.height}
+                src={article.eyecatch.url}
+                alt={`img_${article.title}/`}
+              />
+              <h4 class="title">{article.title}</h4>
+            </a>
+          </div>
+        ))
+      }
     </main>
   </body>
 </html>
```

**詳細画面**
```diff_tsx:src/pages/articles/[...slug].astro
 ---
 import "../../styles/global.css";
+import { getArticles, getArticleDetail } from "../../libs/microcms";

 export async function getStaticPaths() {
-  return [
-    { params: { slug: 1 } },
-    { params: { slug: 2 } },
-    { params: { slug: 3 } },
-  ];
 
+	// 記事ページの全パスを設定
+   const apiResponse = await getArticles({ fields: ["id"] });
+   const articles = apiResponse.contents;
+
+   return articles.map((article) => ({
+   	params: { slug: article.id },
+   }));
 }

+// 設定したパスのパラメータから 記事 ID がセットされた変数を取得
+const { slug } = Astro.params;

+// 記事詳細データを取得
+const article: Article = await getArticleDetail(slug as string);
 ---
 <!doctype html>
 <html lang="ja">
   <meta charset="UTF-8" />
   <meta name="description" content="Astro description" />
   <meta name="viewport" content="width=device-width" />
   <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
     <meta name="generator" content={Astro.generator} />
     <title>Demo Blog | 詳細</title>
   </head>
   <body>
     <main>
-      詳細画面

+      <article>
+        <div>
+          <img
+            width={article.eyecatch.width}
+            height={article.eyecatch.height}
+            src={article.eyecatch.url}
+            alt={`img_${article.title}/`}
+          />
+        </div>
+
+        <h4 class="title">{article.title}</h4>
+        <p set:html={article.content} />
+      </article>
     </main>
   </body>
 </html>
```

直接記載していた画面のパスは、API から取得した値でパスを生成するように変更します。一応、HTML で標準実装されている `<article>` タグを使って、記事コンテンツであることを明示するように記載にしました。

#### API 実行を１回で終わらせる方法
ここでは **一覧取得 API** 👉 **詳細取得 API** という処理フローで記載しており、API を計２回たたいてますが、以下の様に **一覧取得 API のみ** で実装することも可能です。

パターン１：**一覧取得 API と 詳細取得 API を別で実行する場合**（上記と同じ記載）
```tsx
export async function getStaticPaths() {
	// 記事一覧 取得 API 実行
	const apiResponse = await getArticles({ fields: ["id"] });
    const articles = apiResponse.contents;

	return articles.map((article) => ({
		params: { slug: article.id },
	}));
}
const { slug } = Astro.params;

// 記事詳細 取得 API 実行
const article: Article = await getArticleDetail(slug as string);
```

パターン２：**一覧取得 API 実行の１回のみで完了させる場合**（こんな書き方もできる）
```tsx
export async function getStaticPaths() {
	// 記事一覧 取得 API 実行 (フィールド値を指定せず全項目を取得)
	const apiResponse = await getArticles();
    const articles = apiResponse.contents;

	return articles.map((article) => ({
        params: { slug: article.id },
        props: article, // ここで props に渡す
	}));
}

// 記事詳細データとして利用
const article: Article = Astro.props;
```

実は、今回の API のサンプルデータだと「パターン２」でも実装できたのですが、分けて実行する書き方にしました。

両者の違いは、**「一覧取得時のレスポンス（配列の各要素のデータ）と、詳細取得時のレスポンスのデータ内容が一致するかどうか」** です。

一覧取得 API で取得したデータがそのまま詳細画面でも利用できるなら「パターン２」で問題ないですが、詳細取得 API でしか取得できない詳細画面用のデータが存在する場合は「パターン１」で実装しなければなりません。
つまり、CMS 側の API の定義次第です。

#### コンテンツ ID を全て取得する専用メソッドを使用
その他の実装パターンとして、画面のパスを生成するだけの目的なら、ID だけを取得するメソッドも SDK に用意されています。ただし、**limit が効かない** ので使用に注意しましょう。

**ライブラリの取得処理に ID 取得メソッドを追加**
SDK の v2.6.0 から追加された `getAllContentIds()` メソッドを使用します。
```ts:src/libs/microcms.ts
// CMS から API で記事 ID のみを一覧取得する
export const getArticleIds = async (filters?: string) => {
  return await client.getAllContentIds({
    endpoint: endpointArticles,
    filters, // e.g. 'category[equals]uN28Folyn', 'createdAt[greater_than]2021'
  });
}
```
`filters` には、`category[equals]カテゴリID` などと文字列を渡すことで、期間やジャンルなど、該当する特定のコンテンツ ID のみに絞って取得することが可能です。

**画面側のパス定義を変更する**
`getStaticPaths()` の定義を以下のように変更します。
```ts:src/pages/articles/[...slug].astro
// 記事ページの全パスを取得
export async function getStaticPaths() {
  // 記事一覧データを取得する
  const articleIds = await getArticleIds();

  return articleIds.map((id) => ({
    params: { slug: id },
  }));
}
```

上記２つの修正によって、「全コンテンツの静的ページの生成」「特定カテゴリのみの一覧ページの作成」などが可能となります。

#### 動作確認
さて、画面が問題なく表示されるかを確認してみましょう。

**一覧画面** `http://localhost:4321/articles`

![demo_list_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/787582/fe92b9f4-3ab9-f0fb-93c7-f24f4aa27a4a.png)

**詳細画面** `http://localhost:4321/articles/b49k0v7g6`

![demo_detail_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/787582/c2e258dc-9759-8f91-c53a-f032c14a8a67.png)

ページの URL も、ちゃんと CMS 側で発行された ID を含んだ値になっています。

**詳細画面** `http://localhost:4321/articles/rytmup5w4ntx`

![demo_detail_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/787582/e022060f-8cfc-3f92-5fa9-25841fc50607.png)

`<img>` タグを含む、色んなタグを詰め込んだこちらの記事も、問題なく表示できている様です 🎉

今回はスタイルの指定が適当なのでキレイな画面ではないですが、無事に API 経由で記事データを表示するロジックが実装できました 🙌✨

## 実運用する場合の注意点

今回の実装方法は **SSG（静的ジェネレータ）** として Astro を利用をしているので、CMS 側で記事を追加しても、それだけではユーザ画面側に追加記事は **表示されません**。
これは、完全に静的なサイトとして全ページをビルドした状態でサーバにデプロイしているからです。

もし、CMS 側で記事追加など更新作業をした場合は、その度にサイトに最新の状態でビルド・デプロイを実行する手順を、自動または手動で実施する必要があることに注意してください。

CMS 側の記事更新が行われた際に、Webhook を利用して自動デプロイする様に仕組みを作ることができれば、自動化は可能です。

もしくは、Astro を SSR モードで使用し（今回は説明を割愛）、ホスティング側に SSR に対応したサービス・仕組みが存在するなら、それを利用すれば CMS を変更する度にいちいちビルド・デプロイをしなくても、更新内容が自動的に反映されます。

例）Firebase Hosting ＋ Firebase Functions、Cloudflare Pages ＋ Cloudflare Pages Function（または Cloudflare Workers）など

その代わり、**SSR を利用する場合** は、利用するサーバレスサービスが毎回動いてしまいますので、無料枠/課金枠が **運用に耐え得るプランになっているか**（大量アクセスが来ても問題ないか）ご注意ください。

これは人によって状況が違うため、ここではその具体的な方法は説明しません。ご利用している各ホスティングサービスについて調べてみてください。