To update a Next.js site when CMS content is published, receive a signed webhook and call revalidateTag or revalidatePath for the affected data and routes. The tags must match those attached when fetching content. This invalidates caches for subsequent requests; it does not simultaneously replace the page in every visitor's open browser.

This article is written by the genko.me team. It follows the Next.js and genko.me documentation checked on October 20, 2026. For the initial CMS connection, start with using a headless CMS with Next.js.

Should I use revalidateTag or revalidatePath?

Use a tag when several screens share cached data. Use a path when you want to invalidate a particular page or route pattern. Some applications need both because their data and route caches have different scopes.

Call

Scope

Behavior

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

Data with a matching tag

Serves stale data while refreshing in the background

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

Data with a matching tag

The next data request waits for fresh data

revalidatePath('/blog')

The specified page

A Route Handler marks it for revalidation on the next visit

This guide uses { expire: 0 } so that the next tagged fetch after a webhook does not serve stale content. Choose 'max' if response speed matters more than a brief delay in freshness. The single-argument revalidateTag(tag) form is deprecated; use the documented two-argument form in the Next.js reference.

Neither choice pushes a new UI into an already open tab. That distinction is useful when a webhook returns success but someone is still looking at a page they opened before publishing. Check a new request before concluding that cache invalidation failed.

1. Add a cache tag when fetching CMS entries

The example assumes Next.js 16 App Router with Cache Components disabled. It explicitly caches a standard fetch request. Create a list API named blog with a text field named title, publish an entry, and save the following as 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 }[];  }>;}

Replace your-workspace and configure GENKO_API_KEY in the server environment with permission to read the blog API. Keep this file on the server. The @/ imports used here assume that the alias points to the project root.

Call the function from app/blog/page.tsx:

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:blog is an application-defined cache tag, unrelated to editorial content tags. If detail-page fetches also carry this tag, the webhook can invalidate their data together. As the site grows, you may want API-specific or language-specific tags instead of invalidating everything after every edit.

The one-hour time-based revalidation is a fallback when a notification is missed. It is not a promise that every visitor will see an update within exactly one hour: requests and the surrounding cache behavior still matter. Keep an operational recovery path in addition to a cache lifetime.

2. Verify webhook signatures in a Route Handler

Install the SDK for its actual signature-verification helper:

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

Create app/api/genko-webhook/route.ts. Set GENKO_WEBHOOK_SECRET on the server to the same secret you will register in the CMS. This is separate from the delivery API key.

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 });}

The final dynamic route pattern deliberately invalidates all /blog/[id] detail pages. This broad scope helps prevent an old detail page surviving deletion or an entry-ID change. For a large publication, track previous URLs and invalidate a narrower set of paths instead. A detail implementation must still handle a missing or unpublished entry appropriately.

The tag call only works for fetches that carry the same tag. It does not discover untagged requests or cache responses automatically. Keep the fetch configuration and invalidation policy together during review so they cannot silently drift apart.

The SDK verifies the raw request body, before JSON parsing. It checks HMAC-SHA256 over timestamp.rawBody, and its default tolerance rejects timestamps more than 300 seconds away from the current time. Do not parse and reserialize the body before verification. After verification, still check the workspace and API you intend to process. The genko webhook guide documents the headers and payload.

3. Register the webhook and test real publishing

  1. Deploy the handler to a publicly reachable HTTPS URL.

  2. Open the target API's settings and add a webhook.

  3. Enter the final receiver URL and matching secret.

  4. Send a test notification and inspect the response.

  5. Save and publish a real entry, then check the list and detail pages.

The receiver must be public: localhost and private-network addresses are not valid destinations. genko does not follow redirects, so register the final URL rather than relying on a redirect to your handler.

A test payload uses event: 'webhook.test' and entry: null. The handler above acknowledges a valid test without invalidating content. A successful test therefore proves delivery and signature configuration, not the complete publishing workflow. Follow it with a real content change.

Why listening only for entry.published misses updates

genko's event names distinguish manual publication from scheduled publication. Manually saving an entry as published produces entry.created or entry.updated. entry.published is emitted when scheduled publication is finalized by the periodic process.

The handler includes creation, updates, deletion, scheduled publication, and scheduled unpublication. Do not filter everything to status === 'published': changing a previously published entry back to draft also requires invalidating the old public page, even though the new status is draft.

Saving unpublished changes to an already published entry as a draft does not send a webhook. Saving those changes into the public version does. Explain this distinction to editors so a preview-only save is not mistaken for a failed production update.

For multilingual APIs, sublanguage notifications include lang. Add the corresponding language-specific paths and tags when supporting translated routes. The example above handles the single /blog route tree; it is not a complete multilingual invalidation policy.

Why does the webhook succeed but the page stay stale?

Check delivery first, then the data cache, route cache, and browser state. A 200 response only means your receiver accepted the notification. Common causes include mismatched tags, a separate CDN caching HTML, or checking a tab that has not made another request.

If you use a caching proxy in front of Next.js, it needs an explicit freshness policy too. Invalidating a Next.js data tag does not automatically purge every independent cache between the application and the visitor. Similarly, a static export has no running App Router handler to receive this request.

The official path reference explains that invalidation from a Route Handler is applied on the next visit. Use this expected behavior when designing an acceptance check: publish, confirm receipt, then make a fresh request for the affected route.

How do I recover from a failed webhook?

genko webhook requests time out after 10 seconds and do not retry automatically. Inspect the delivery log and manually resend the failed notification after fixing the receiver. A resend keeps the original payload but receives a new signature and timestamp.

The receiver should tolerate repeated processing of the same content change. The example only invalidates caches, so repeated calls do not repeat a content mutation. If you add expensive work, move it behind an appropriate queue rather than holding the webhook response open.

For path-only invalidation, the SDK's nextRevalidate plugin offers a shorter integration. It verifies the signature and calls revalidatePath() for the returned paths. It does not create cache tags or rebuild a statically hosted site.

FAQ

Does a CMS webhook regenerate every page immediately?

No. Revalidation from a Route Handler prepares affected caches for subsequent requests. It does not broadcast a replacement page to every open browser.

Do I always need both revalidateTag and revalidatePath?

No. Match the invalidation to the cache you actually created. Shared data often benefits from tags, while a particular page can be handled by path.

Can this work with a Next.js static export?

This receiver needs a server. For static hosting, trigger a rebuild or use browser fetching in plain HTML.

Does a successful test notification prove publishing works?

No. Test notifications have no entry, and this handler acknowledges them without revalidation. Verify actual publishing, editing, and unpublishing separately.

Sources

Checked October 20, 2026.