こんにちは!株式会社アルファドライブでエンジニアをしているshunsukeと申します。
今回はNext.js App Router + microCMSで記事の更新時にキャッシュを再検証する方法をご紹介したいと思います。
この記事は「AlphaDrive Advent Calendar 2023」の5日目のエントリーです。
背景
この記事を書こうと思ったきっかけ
弊社のプロジェクトとして、Next.js App Router + microCMSを使用してWebメディアを開発することになりました。Webメディアであれば記事を更新したときに内容が即時反映されると嬉しいので、その機構を作ろうと思ったことがきっかけです。
※ 詳しくは、AlphaDrive Advent Calendar 2023 4日目のエントリーで佐藤さんが書いてくださっているので、興味のある方はご一読くださると幸いです。
全体感
ざっくり言うと、以下のフローで記事の更新時にキャッシュが再検証されます。
- microCMSで記事を更新
- microCMSからWebhookを介してNext.jsのAPIへリクエストを送信
- APIでデータキャッシュの再検証を行う
- ページが更新される
手順
1.Webhookを受けるためのAPIエンドポイントを作る
まずはmicroCMSからのWebhookを受けるAPIを作成します。
POSTリクエストを受け付ける必要があります。公式ドキュメントを参考に簡単に作ってみます。
export async function POST(req: Request) {
console.log("hello world")
}
microCMS側でWebhookが送信される操作を行い、疎通確認してみてください。
2. microCMS設定画面からWebhookを作る
microCMS側で記事を更新したときにWebhookを送信するように設定します。
サービスの種類は「カスタム通知」を選択します。
以下の各項目を入力していきます。
- Webhookの名称
お好きな名称を入力してください - Webhookを送信するエンドポイント
先程作成したAPIのエンドポイントを入力してください。 - シークレット
Webhookの送信元がmicroCMSであることを検証するため、Signatureの生成に利用するシークレット値を入力します。Webhookでリクエストを送信した時、X-MICROCMS-Signature
とう名称のヘッダに付与されるハッシュ値の元となる値です。送信元を検証しないとmicroCMS以外からのAPIリクエストでも再検証処理が発火してしまう可能性があるので入力しておいたほうが良いです。
ドキュメントに値の生成方法などが記載されているので、参考にすると良さそうです。 - カスタムリクエストヘッダ
個別のユースケースで必要であれば入力してください。今回は不要なのでスキップします。 - 通知タイミングの設定
コンテンツの公開に関係するものを全て選択します
3. signature secret を検証する
このAPIはmicroCMSからのリクエストのみを受け付けるべきなので、検証処理をはさみます。
先述の通りmicroCMSのWebhookにシークレット値を入力した場合、 X-MICROCMS-Signature
というカスタムヘッダーが付与されています。ここには、シークレットが暗号化された値が入っているため、この値を利用します。
また、Webhookに設定したシークレットと同じ値をNext.jsの環境変数に埋め込んでおきます。
実装方法はmicroCMSの公式ドキュメントを参考に作ります。
import { revalidateTag } from "next/cache";
import { NextResponse } from "next/server";
import * as crypto from "crypto";
export async function POST(request: Request): Promise<Response> {
const bodyText = await request.text();
const bodyBuffer = Buffer.from(bodyText, "utf-8");
const secret = process.env.MICROCMS_WEBHOOK_SIGNATURE_SECRET;
if (!secret) {
console.error("Secret is empty.");
return NextResponse.json({
status: 500,
});
}
const signature = request.headers.get("X-MICROCMS-Signature");
if (!signature) {
console.error("Signature is empty.");
return NextResponse.json({
status: 400,
});
}
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(bodyBuffer)
.digest("hex");
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
if (!isValid) {
console.error("Invalid signature.");
return NextResponse.json({
status: 400,
});
}
// 再検証処理(後述します)
return NextResponse.json({ message: "success" });
}
これで設定したシークレットを持つリクエストのみを受け付けるようになりました。
4.revalidateする
続いて、データキャッシュをrevalidateする処理を書いていきます。
この処理はNext.jsのOn-demand revalidationという機構を使用します。
revalidateTag もしくはrevalidatePathという関数を使用することで実現できますが、今回はrevalidateTagを採用します。
revalidateTag("articles")
この関数を先程作ったAPIの末尾に追記します。
5.コンテンツ取得処理に tag をつける
revalidateTagで指定した文字列を、コンポーネント内でコンテンツ取得しているfetch関数のオプションのtagsでも指定します。
const contents = await fetch("your-microcms-api-endpoint", {
headers: {
"X-MICROCMS-API-KEY": "your-microcms-api-key",
},
next: {
tags: ["articles"],
}
})
microcms-js-sdkを使用している場合は以下のようになります。
const contents = await client.getList({
endpoint: "contents",
customRequestInit: {
next: { tags: ["articles"] },
},
}),
これでmicroCMSで記事を更新したときにリクエストのキャッシュを再検証できるようになりました👏
※ 後述しますが、tagの命名規則はmicroCMS上のAPIのエンドポイントと同じにするのがおすすめです。例えば、 /articles
というAPIなら 上記のように”articles” というtagを指定します。
6.1つのrevalidate APIで複数のコンテンツのrevalidateに対応する
上述した実装でコンテンツの更新時にキャッシュが再検証され、ページが再生成されるようにはなりました。しかし今のままではmicroCMS側のAPIが増えるたびNext.js側でも同じようなAPIを作らなければいけません。
例えば、microCMS側で”authors”というAPIを作成した場合、Next.js側でも”authors”へのリクエストを行って取得したデータの再検証を行うAPIを作る必要があります。 それは大変なので、Next.js側のAPI1つで複数のコンテンツのrevalidateができるようにします。
それを実現するためには、Webhookのリクエストボディに含まれている情報を利用します。
リクエストボディのapiプロパティには、Webhookを設定したエンドポイントと同じ文字列が入ってきます。この値を利用して、先程作ったAPIを全コンテンツのrevalidate用APIに変更します。
※ 例: エンドポイントを”category”で設定しているなら”cateogry”という文字列が入ります。
import { revalidateTag } from "next/cache";
import { NextResponse } from "next/server";
import * as crypto from "crypto";
type RequestBody = {
id: string | null | undefined;
api: string;
};
export async function POST(request: Request): Promise<Response> {
const bodyText = await request.text();
const bodyBuffer = Buffer.from(bodyText, "utf-8");
const { id, api: endpoint } = JSON.parse(bodyText) as RequestBody;
if (!bodyText) {
console.error("Body is empty.");
return NextResponse.json({
status: 400,
});
}
const secret = process.env.MICROCMS_WEBHOOK_SIGNATURE_SECRET;
if (!secret) {
console.error("Secret is empty.");
return NextResponse.json({
status: 500,
});
}
const signature = request.headers.get("X-MICROCMS-Signature");
if (!signature) {
console.error("Signature is empty.");
return NextResponse.json({
status: 400,
});
}
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(bodyBuffer)
.digest("hex");
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
if (!isValid) {
console.error("Invalid signature.");
return NextResponse.json({
status: 400,
});
}
if (id) {
revalidateTag(`${endpoint}/${id}`);
}
revalidateTag(endpoint);
return NextResponse.json({ message: "success" });
}
microCMSのWebhookのエンドポイント全てに、このAPIのエンドポイントを指定します。
またfetch関数を使用するとき、tagsにはエンドポイントと同じ名称のものを使用します。
こうすることで、このAPI1つでWebhookを設定したmicroCMS APIへのデータキャッシュのrevalidateが行なえるようになります👏
感想
AppRouterの正式版がリリースされてからしばらく経ちますが、実務で触れることができて楽しかったです。同じようなことでお困りの方の一助となれば幸いです!