
なぜオンデマンドが必要か
Next.js 16のApp Routerで静的生成したページは、ビルド時点のデータで固定されます。
ヘッドレスCMSのStrapi v5で記事を更新しても、再デプロイしない限り本番には反映されません。
時間ベースのISR(revalidate指定)だけに頼ると、最短でも数十秒から数分の遅延が発生します。
編集者が「今すぐ公開したい」場合に、この遅延は体験として許容できません。
解決策がオンデマンドリバリデーションです。Strapi側のwebhookからNext.js側のAPIを叩き、該当パスだけを即座に再生成します。
Strapi v5 webhookの設定
Strapiの管理画面でSettings → Webhooks → Create new webhookを開きます。
URLにはNext.js側のエンドポイント、例えば https://netsujo.jp/api/revalidate を指定します。
HeadersにはWebhookシークレットを追加します。キー名は x-webhook-secret のような明示的な名前を推奨します。
Eventsは Entry のうち publish / update / unpublish / delete にチェックを入れます。
Content-Type判定が必要な場合は、Strapiから送られてくるペイロードに model フィールドが含まれるため、それをNext.js側でルーティングに使います。
Next.js側の /api/revalidate 実装
App Routerの場合、app/api/revalidate/route.ts にRoute Handlerを配置します。
署名検証、モデル別の分岐、revalidatePathの呼び出しを1ファイルにまとめます。
import { NextRequest, NextResponse } from "next/server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function POST(req: NextRequest) {
const secret = req.headers.get("x-webhook-secret");
if (secret !== process.env.STRAPI_WEBHOOK_SECRET) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}
const body = await req.json();
const model = body?.model;
const slug = body?.entry?.slug;
if (model === "blog-post") {
revalidatePath("/blog");
if (slug) revalidatePath(`/blog/${slug}`);
}
if (model === "page") {
revalidatePath("/");
}
return NextResponse.json({ revalidated: true, model, slug });
}revalidatePathはApp Routerのfetchキャッシュとページキャッシュを同時に無効化します。
動的ルートの場合はパス文字列を正しく組み立てる必要があります。
webhookシークレットの管理とローテーション
シークレットはStrapi側とNext.js側の両方に保存するため、ローテーション時に同期ずれが起きやすい項目です。
Netsujoでは長さ64文字のランダム16進文字列を使い、Vercelの環境変数 STRAPI_WEBHOOK_SECRET に保存しています。
ローテーション手順は次の通りです。
- 新しいシークレットを生成する(openssl rand -hex 32)
- 先にVercel環境変数を更新する
- Vercelの再デプロイを待つ
- Strapi側のwebhook設定を新しいシークレットに差し替える
- test runボタンで200が返ることを確認する
順序を逆にするとStrapiからのwebhookが401を返す時間帯が発生します。
はまりどころ: Vercel env末尾の改行
Netsujoが実際に踏んだ事故を紹介します。
Vercel CLIで環境変数を登録する際に、echo を使ってパイプで値を渡すと末尾に改行コードが混入します。
echo "fe3bcf47..." | vercel env add STRAPI_WEBHOOK_SECRET productionこの形式で登録すると、環境変数の値が fe3bcf47...\n となり、Strapiから送られてくる値と一致せず全件401になります。
ログにはシークレット長が65文字と出ていて、期待値の64文字より1バイト多いことで気付きました。
解決策は --value フラグで直接渡す形式に変えることです。
vercel env add STRAPI_WEBHOOK_SECRET production --value "fe3bcf47..." --yesまたは printf を使います。echo と違い printf は末尾の改行を付けません。
printf "%s" "fe3bcf47..." | vercel env add STRAPI_WEBHOOK_SECRET production登録後は vercel env pull で .env.production.local に取得し、cat -A でダンプして末尾の $ マークが付いていないことを必ず確認します。
はまりどころ: 環境変数キャッシュ
Vercelは環境変数を更新しても、既存のデプロイには反映されません。
新しい値を読み込むには再デプロイが必要です。
Production環境に即時反映したい場合は vercel --prod でデプロイするか、ダッシュボードから Redeploy を実行します。
Preview環境は個別のURLごとにキャッシュが残るため、忘れず再デプロイします。
はまりどころ: revalidatePathのパス指定
動的ルートの場合、revalidatePathの第2引数に 'page' や 'layout' を指定できます。
指定しないとデフォルトで該当ルートのページキャッシュだけが無効化されます。
一覧ページと詳細ページの両方を更新したい場合は、両方のパスを明示的に呼び出します。
revalidatePath("/blog");
revalidatePath(`/blog/${slug}`);まとめ
Next.js 16 × Strapi v5のリバリデーションは、webhookとRoute Handlerを組み合わせれば数十行で実装できます。
難しいのは実装ではなく運用面で、環境変数の末尾改行やデプロイキャッシュのような地雷を踏みやすいです。
Netsujoでは本番監視で401レートを可視化し、異常があればSlackに通知する体制にしています。
webhookは疎通確認できる仕組みを初日から組み込むことが重要です。







