Next.js でキャッシュを再検証する
Webhook と Next.js の revalidateTag() を組み合わせて、更新されたコンテンツのキャッシュだけを失効させる構成を説明します。
時間ベースの再検証では、更新がなくても再取得が走り、逆に更新直後は次の再検証まで古い内容が表示されます。Webhook で失効させると、更新があったコンテンツだけを対象にできます。
このページは Next.js 16 の App Router を前提としています。
仕組み
- 編集者が CMS でコンテンツを公開する
- Orizm が設定された URL に POST リクエストを送る
- Route Handler がペイロードから失効させるキャッシュタグを決定する
revalidateTag()でそのタグを失効させる- 次のリクエストで最新のコンテンツが取得される
キャッシュタグを設計する
一覧ページ用の「テーブル単位のタグ」と、詳細ページ用の「コンテンツ単位のタグ」の2種類があれば、ほとんどのサイトで足ります。
import type { OrizmClient } from "@repo/orizm/consumer";
export function generateTag(
tableName: keyof OrizmClient["tables"],
resourceId?: string,
): string {
return resourceId ? `${tableName}:${resourceId}` : tableName;
}テーブル名を keyof OrizmClient["tables"] で受け取ることで、存在しないテーブル名を渡すとコンパイルエラーになります。
コンテンツ取得にタグを付ける
Consumer SDK の各メソッドは最後の引数で RequestInit を受け取ります。ここで next.tags を指定すると、その取得結果にキャッシュタグが付きます。
import { serverSideCmsClient } from "@/lib/orizm-client";
import { generateTag } from "@/lib/revalidate-tag";
export function getBlogList() {
return serverSideCmsClient.tables.blog.list(
{ order: { publishedAt: "desc" } },
{ next: { tags: [generateTag("blog")] } },
);
}詳細ページにはコンテンツ単位のタグだけを付けます。失効させる側で必要なタグをまとめて渡すため、取得側は1つのタグで済みます。
import { serverSideCmsClient } from "@/lib/orizm-client";
import { generateTag } from "@/lib/revalidate-tag";
export function getBlogSingle(id: string) {
return serverSideCmsClient.tables.blog.get(
id,
{ include: { category: true } },
{ next: { tags: [generateTag("blog", id)] } },
);
}RequestInit の指定方法は コンテンツの取得 を参照してください。
Route Handler を実装する
Webhook を受け取るエンドポイントを作ります。ペイロードの型 WebhookPayload は orizm codegen consumer が生成します。table をキーにした判別可能なユニオンなので、switch で分岐すると content の型がそのテーブルの型に絞られます。
送信元の確認には 署名の検証 を使います。jwksUrl に設定する値は後述の エンドポイントを保護する を参照してください。
import type { WebhookPayload } from "@repo/orizm/consumer";
import { createWebhookVerifier } from "@orizm/consumer-sdk/webhook";
import { revalidateTag } from "next/cache";
import { type NextRequest, NextResponse } from "next/server";
import { generateTag } from "@/lib/revalidate-tag";
import { env } from "@/env";
// リクエストごとに作り直すと JWKS のキャッシュが効かないため、モジュールスコープに置く
const verifier = createWebhookVerifier<WebhookPayload>({
jwksUrl: env.ORIZM_WEBHOOK_JWKS_URL,
// インスタンスが使い捨てられてもキャッシュが効くよう、JWKS の取得を Data Cache に載せる
fetchOptions: { next: { revalidate: 300 } },
});
export const POST = async (request: NextRequest): Promise<Response> => {
const result = await verifier.verifyRequest(request);
if (!result.valid) {
return NextResponse.json({ error: result.reason }, { status: 401 });
}
const revalidateTags = await getRevalidateTags(result.body);
for (const tag of revalidateTags) {
revalidateTag(tag, "max");
}
return NextResponse.json({ revalidated: revalidateTags });
};
const getRevalidateTags = async (
payload: WebhookPayload,
): Promise<string[]> => {
switch (payload.table) {
case "blog": {
return [generateTag("blog"), generateTag("blog", payload.content.id)];
}
// ...
default: {
assertNever(payload);
}
}
};
function assertNever(x: never): never {
throw new Error(`Unexpected payload: ${JSON.stringify(x)}`);
}実装時の注意は4点です。
verifyRequestは署名検証のために body を読み切るため、呼び出したあとにrequest.json()を呼ぶことはできません。ペイロードはresult.bodyから取得します- JWKS が取得できないときは
WebhookJwksFetchErrorが throw されます。捕捉せずに Next.js に 500 を返させれば、Orizm が再送します。ただし JWKS URL が 404 を返す場合は設定間違いなので、reasonがjwks-not-foundの検証失敗として返ります - 配信は5秒でタイムアウトします。タグを組むために参照先を引く必要がある場合も、軽量な取得1回程度に留めてください
- 同じイベントが複数回届くことがあります。
revalidateTag()は何度呼んでも問題ありませんが、それ以外の副作用を行う場合は重複実行を考慮してください
配信の詳細は 配信の仕様 を参照してください。
エンドポイントを保護する
このエンドポイントは公開 URL に置くため、Orizm 以外からのリクエストを拒否する必要があります。Orizm はリクエストに Ed25519 署名を付与するため、署名の検証で送信元を確認します。
検証に使う JWKS の URL は、Orizm コンソールの Webhook 詳細画面に表示されます。
この URL が返すのは公開鍵だけなので、秘密の値として扱う必要はありません。ソースコードに直接書いても構いませんし、環境ごとに Webhook を分けていて URL が変わる場合は、環境変数にしておくと扱いやすくなります。
ORIZM_WEBHOOK_JWKS_URL=<コンソールに表示される JWKS URL>署名の検証に失敗して401を返すと、その通知はリトライされずに失われます。設定を直したあとは、改めてコンテンツを更新して配信を発生させてください。
Orizm に Webhook を登録する
エンドポイントを用意したら、Orizm 側に通知先として登録します。開発者コンソールでの手順は 設定方法 を参照してください。
CLI からも登録できます。
orizm webhook create --name "revalidate" \
--url https://example.com/api/revalidate \
--table-event blog:content.create \
--table-event blog:content.update \
--table-event blog:content.delete購読するテーブルとイベントは、サイトの表示に使っているものだけに絞ります。どの編集操作でどのイベントが届くかは 編集操作とイベントの対応 を参照してください。
あとからスキーマにテーブルを追加してサイトの表示に使う場合は、Table Events への追加も必要です。追加を忘れると、そのテーブルの更新は通知されません。
デプロイ時の注意
- Basic 認証やアクセス制限から除外する。 Next.js 16 の
proxy.ts(Next.js 15 以前のmiddleware.ts) で Basic 認証をかけている場合、matcherから/api/revalidateを除外してください - Vercel の Deployment Protection を確認する。 保護が有効な環境では外部からの POST が拒否されます。保護をバイパスする設定を行うか、本番環境のみに Webhook を登録してください
うまく動かないとき
再検証されない
- 開発サーバーではキャッシュの挙動が本番と異なります。デプロイした環境で確認してください
- 取得側に
next.tagsを付け忘れていないか確認してください。タグが付いていないキャッシュはrevalidateTag()の対象になりません - Route Handler が返した
revalidatedに意図したタグが含まれているか、ログで確認してください revalidateTag()の第2引数に"max"を指定している場合、そのページが次に表示されるまで再取得は始まりません- 予約公開・予約非公開を使っている場合、指定した時刻から最大1分ほど遅れて通知されます
401が返る
ORIZM_WEBHOOK_JWKS_URLが、通知を受け取りたい Webhook の JWKS URL と一致しているか確認してください。JWKS は Webhook ごとに異なるため、別の Webhook の URL では検証に失敗しますverifyRequestより先にrequest.json()などで body を読んでいないか確認してください- Basic 認証やアクセス制限がエンドポイントに掛かっていないか確認してください