Next.jsでCMSの記事公開に合わせて表示を更新するには、署名を検証したWebhookからrevalidateTagやrevalidatePathを呼びます。取得時に付けたタグと、一覧・詳細のURLを更新対象にしてください。Webhookはキャッシュを無効化する通知であり、開いている全ブラウザの表示を同時に書き換える機能ではありません。

本記事はgenko.meの運営側が執筆しています。2026年10月20日に確認したNext.jsの公式仕様とgenko.meのWebhook仕様を使い、App Routerの受信処理を作ります。接続の基本はNext.jsでヘッドレスCMSを使うを参照してください。

revalidateTagとrevalidatePathはどう使い分けますか

Next.js公式のrevalidatePathでは、Route Handlerからの呼び出しは次回アクセス時の再検証として説明されています。

呼び出し

主な対象

表示への影響

revalidateTag('genko:blog', 'max')

同じタグのデータ

古い内容を返しながらバックグラウンドで更新

revalidateTag('genko:blog', { expire: 0 })

同じタグのデータ

次の取得で新しいデータを待つ

revalidatePath('/blog')

一覧ページ

次にそのパスを訪れたとき再検証

この記事ではWebhook受信後の次の取得で古いデータを返さないため、{ expire: 0 }を使います。応答速度を優先して一時的な古さを許容するなら'max'が候補です。1引数だけのrevalidateTag(tag)は非推奨です。詳細は公式リファレンスを確認してください。

1. 記事取得にキャッシュタグを付ける

Next.js 16のApp Routerを前提にします。以下はCache Componentsを有効にしていない構成で、標準のfetchにキャッシュを明示する例です。lib/posts.tsへ保存します。

ts
import 'server-only';
export async function getPosts() {  const key = process.env.GENKO_API_KEY;  if (!key) throw new Error('GENKO_API_KEY is not configured');  const url = new URL('https://api.genko.me/v1/your-workspace/blog');  url.searchParams.set('orders', '-publishedAt');  url.searchParams.set('limit', '10');  const response = await fetch(url, {    headers: { 'X-API-KEY': key },    cache: 'force-cache',    next: { tags: ['genko:blog'], revalidate: 3600 },  });  if (!response.ok) throw new Error(`Delivery API: ${response.status}`);  return response.json() as Promise<{    contents: { id: string; title: string }[];  }>;}

app/blog/page.tsxから呼び出します。titleというテキストフィールドを持つリスト形式のblogAPIと、公開済み記事を用意してください。

tsx
import { getPosts } from '@/lib/posts';
export default async function BlogPage() {  const { contents } = await getPosts();  return <ul>{contents.map(post => <li key={post.id}>{post.title}</li>)}</ul>;}

GENKO_API_KEYはサーバーで設定し、ワークスペースIDも置き換えます。@/はプロジェクトルートの想定です。

2. Webhookの署名を検証する受信ルートを作る

SDKを追加します。署名検証後に再検証を呼びます。

bash
npm install @genko-me/sdk server-only

app/api/genko-webhook/route.tsを作り、サーバーのGENKO_WEBHOOK_SECRETにCMSへ登録するものと同じシークレットを設定します。配信APIキーとは別の値です。

ts
import { revalidatePath, revalidateTag } from 'next/cache';import { verifyGenkoSignature } from '@genko-me/sdk';
const events = new Set([  'entry.created', 'entry.updated', 'entry.deleted',  'entry.published', 'entry.unpublished',]);
export async function POST(request: Request) {  const secret = process.env.GENKO_WEBHOOK_SECRET;  if (!secret) return new Response('Missing secret', { status: 500 });  const rawBody = await request.text();  const valid = await verifyGenkoSignature({    secret,    rawBody,    signatureHeader: request.headers.get('x-genko-signature'),    timestampHeader: request.headers.get('x-genko-timestamp'),  });  if (!valid) return new Response('Invalid signature', { status: 401 });
  let payload;  try { payload = JSON.parse(rawBody); }  catch { return new Response('Invalid JSON', { status: 400 }); }  if (!payload || typeof payload !== 'object' ||      payload.workspace !== 'your-workspace' || payload.api !== 'blog') {    return new Response('Unexpected source', { status: 400 });  }  if (payload.event === 'webhook.test') {    return Response.json({ received: true, test: true });  }  if (!events.has(payload.event)) {    return Response.json({ received: true, ignored: true });  }  if (typeof payload.entry?.id !== 'string' || !payload.entry.id) {    return new Response('Missing entry ID', { status: 400 });  }  revalidateTag('genko:blog', { expire: 0 });  revalidatePath('/blog');  revalidatePath('/blog/[id]', 'page');  return Response.json({ revalidated: true });}

動的パターンは、削除やID変更で古いページが残らないよう詳細全体を対象にします。大規模サイトでは変更前URLを管理して個別パスへ絞ります。タグは取得側と一致させます。

署名はJSONを再生成した文字列ではなく、生のrawBodyで検証します。SDKはtimestamp.rawBodyのHMAC-SHA256を確認し、既定では現在時刻と300秒を超えてずれた通知を拒否します。Webhookガイドにヘッダーと配送の仕様があります。

3. genko.meにWebhookを登録する

  1. 受信ルートを外部から届くHTTPSのURLへデプロイします。

  2. 対象APIの「設定」で「Webhookを追加」を開きます。

  3. 最終的な受信URLとシークレットを登録します。

  4. 「テスト送信」で受信確認をします。

  5. 実際の記事を保存・公開し、一覧と詳細の更新を確認します。

genkoはリダイレクトを追跡しません。localhostやプライベートアドレスも通知先にはできません。テスト通知はwebhook.testでentryがnullです。上のルートでは署名と送信元の確認だけを行い、テストではキャッシュを変更しません。

公開イベントだけを受けると更新を見落とします

手動公開はentry.createdかentry.updated、予約公開の確定はentry.publishedです。後者だけを待つと手動公開を見落とします。公開中の原稿を下書きとして保存するだけではWebhookは送られません。

公開から下書きへ戻した場合もentry.updatedが届きます。status === 'published'の場合だけ処理すると、公開済みページの削除が反映されません。削除、公開終了も含めて無効化してください。

多言語ではサブ言語の通知にlangが付くため、英語版などのルートも更新対象に含めます。上の例は日本語の/blogだけを扱います。

Webhookが成功しても古いページが出るとき

タグが違う、別ホストのCDNがHTMLをキャッシュしている、開いたタブの表示を見ている、といった場合はHTTP 200でも表示が変わりません。

genkoのWebhookは10秒でタイムアウトし、自動リトライはありません。配信ログから失敗した通知を手動で再送できます。再送時は元の本文に新しい署名とタイムスタンプが付きます。同じ通知を再度受けても処理できるようにします。

よくある質問

Webhookを送るとページはその瞬間に再生成されますか?

Route Handlerからの再検証は、基本的に次のアクセスや取得に備えた無効化です。閲覧中のブラウザへ新しい画面を一斉配信するものではありません。

revalidateTagとrevalidatePathは両方必要ですか?

必ず両方ではありません。複数画面で共有する取得結果はタグ、特定URLの更新はパスを使い、実際のキャッシュ設計に合わせます。

static exportしたNext.jsでも使えますか?

この受信処理にはサーバーが必要です。静的ファイルだけを配信する場合はWebhookで再ビルドするか、静的HTMLからAPIを取得する方法を使います。

テスト送信が成功すれば公開反映も確認できますか?

いいえ。テストには記事情報がなく、この例は更新処理をしません。記事の実際の公開・更新・非公開化を別途確認してください。

出典

2026年10月20日に確認しました。