概要
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つ分のレスポンスデータは、以下です。
{
"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
がファイルを用意し、以下の様に値を設定しておきます。
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 で画面が表示できる最低限の画面を書きます。
一覧画面
---
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>
タグで書いたりすれば不要です。
詳細画面
---
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
無事に表示ができました。
尚、定義していないルーティングページ(例: /articles/4
とか)だと、静的ページが生成されていないため、"404"(ページが見つからない)エラー画面が表示されます。
API 実行処理を作る
libs
というディレクトリを src
直下に作成して、microcms.ts
というファイルを作り、API を実行するライブラリとして処理を実装します。尚、今回は、TypeScript で書きます。
(名前は libs
でなくても library
でも service
でもなんでも大丈夫です)
ここに、環境変数から値を取得して API に利用する値を定義します。
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>()
を使用する場合の書き方
// 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 ArticleResponse
の contents
で指定した通り Article[]
型となります。
しかし、getList<T>()
の場合は、最終的に取得できる配列データ contents
は自動的に (T & MicroCMSContentId & MicroCMSDate)[]
型となります。
つまり、今回の例で言うと (Article & MicroCMSContentId & MicroCMSDate)[]
型になります(型が拡張されて、コンテンツ ID なども取得できる感じ)。
どちらを採用するかはお好みで
どちらを使うかはお好みですが、利用したいデータが配列であると決まっている場合は getList<T>()
が使いやすいかと思います。
画面で API 処理を呼ぶ
.astro ファイルの ---
領域の中にサーバサイド処理を書けるので、そこに先ほど実装した記事取得処理を追加します。
また、取得した記事をループ処理で画面に表示する処理も記載します。
本来は、適切なタグを選んで、スタイル整えて <ul>
や <li>
で並べるべきですが、今回はわかりやすく適当に <div>
タグで羅列します。
一覧画面
---
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>
詳細画面
---
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()
メソッドを使用します。
// 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()
の定義を以下のように変更します。
// 記事ページの全パスを取得
export async function getStaticPaths() {
// 記事一覧データを取得する
const articleIds = await getArticleIds();
return articleIds.map((id) => ({
params: { slug: id },
}));
}
上記2つの修正によって、「全コンテンツの静的ページの生成」「特定カテゴリのみの一覧ページの作成」などが可能となります。
動作確認
さて、画面が問題なく表示されるかを確認してみましょう。
一覧画面 http://localhost:4321/articles
詳細画面 http://localhost:4321/articles/b49k0v7g6
ページの URL も、ちゃんと CMS 側で発行された ID を含んだ値になっています。
詳細画面 http://localhost:4321/articles/rytmup5w4ntx
<img>
タグを含む、色んなタグを詰め込んだこちらの記事も、問題なく表示できている様です 🎉
今回はスタイルの指定が適当なのでキレイな画面ではないですが、無事に API 経由で記事データを表示するロジックが実装できました 🙌✨
実運用する場合の注意点
今回の実装方法は SSG(静的ジェネレータ) として Astro を利用をしているので、CMS 側で記事を追加しても、それだけではユーザ画面側に追加記事は 表示されません。
これは、完全に静的なサイトとして全ページをビルドした状態でサーバにデプロイしているからです。
もし、CMS 側で記事追加など更新作業をした場合は、その度にサイトに最新の状態でビルド・デプロイを実行する手順を、自動または手動で実施する必要があることに注意してください。
CMS 側の記事更新が行われた際に、Webhook を利用して自動デプロイする様に仕組みを作ることができれば、自動化は可能です。
もしくは、Astro を SSR モードで使用し(今回は説明を割愛)、ホスティング側に SSR に対応したサービス・仕組みが存在するなら、それを利用すれば CMS を変更する度にいちいちビルド・デプロイをしなくても、更新内容が自動的に反映されます。
例)Firebase Hosting + Firebase Functions、Cloudflare Pages + Cloudflare Pages Function(または Cloudflare Workers)など
その代わり、SSR を利用する場合 は、利用するサーバレスサービスが毎回動いてしまいますので、無料枠/課金枠が 運用に耐え得るプランになっているか(大量アクセスが来ても問題ないか)ご注意ください。
これは人によって状況が違うため、ここではその具体的な方法は説明しません。ご利用している各ホスティングサービスについて調べてみてください。