4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2024-02-11

概要

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

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

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

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

前提

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

また、今回利用する CMS は microCMS という日本産のヘッドレス CMS です。microCMS は、API 利用が簡単にできる様に JavaScript 用の SDK (microcms-js-sdk)を用意されており、今回はそれを使用します。

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

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

ブログ1記事のデータ
{
    "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 のレスポンスは以下です。

ブログ記事一覧のデータ
{
  "contents": [
    {
      // 上記の1記事分と同じデータ内容
      "id": "b49k0v7g6",
        :
    },
    {
      // 上記の1記事分と同じデータ内容
      "id": "rytmup5w4ntx",
        :
    }
  ],
"totalCount": 2,
"offset": 0,
"limit": 10
}

手順

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

SDK をインストール

microCMS が提供している SDK をインストールしておきます。

npm install microcms-js-sdk

npm ではなく yarn を使っている場合、以下のコマンドです。

yarn add microcms-js-sdk

一応、CDN での配布もされていますので、CDN の場合はヘッダに含めましょう。

<script src="https://unpkg.com/microcms-js-sdk@latest/dist/umd/microcms-js-sdk.js"></script>

環境変数の設定

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

.env
MICROCMS_SERVICE_DOMAIN=<YOUR_SERVICE_DOMAIN>
MICROCMS_API_KEY=<API_KEY>

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

画面の作成

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

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

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

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

一覧画面

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> タグで書いたりすれば不要です。

詳細画面

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

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

API 実行処理を作る

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

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

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>() を使用する場合の書き方

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

get メソッド と getList メソッドの違い

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

❶ 指定する型が変わる
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 ArticleResponsecontents で指定した通り Article[] 型となります。

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

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

画面で API 処理を呼ぶ

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

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

一覧画面

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>

詳細画面

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 実行を1回で終わらせる方法

ここでは 一覧取得 API 👉 詳細取得 API という処理フローで記載しており、API を計2回たたいてますが、以下の様に 一覧取得 API のみ で実装することも可能です。

パターン1:一覧取得 API と 詳細取得 API を別で実行する場合(上記と同じ記載)

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);

パターン2:一覧取得 API 実行の1回のみで完了させる場合(こんな書き方もできる)

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 のサンプルデータだと「パターン2」でも実装できたのですが、分けて実行する書き方にしました。

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

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

コンテンツ ID を全て取得する専用メソッドを使用

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

ライブラリの取得処理に ID 取得メソッドを追加
SDK の v2.6.0 から追加された getAllContentIds() メソッドを使用します。

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() の定義を以下のように変更します。

src/pages/articles/[...slug].astro
// 記事ページの全パスを取得
export async function getStaticPaths() {
  // 記事一覧データを取得する
  const articleIds = await getArticleIds();

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

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

動作確認

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

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

demo_list_1.png

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

demo_detail_1.png

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

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

demo_detail_2.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 を利用する場合 は、利用するサーバレスサービスが毎回動いてしまいますので、無料枠/課金枠が 運用に耐え得るプランになっているか(大量アクセスが来ても問題ないか)ご注意ください。

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

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?