Next.js ISR + Webhooks: Cache Invalidation with a Headless CMS

A practical guide to combining Next.js ISR and CMS webhooks so content updates go live instantly without rebuilding the entire site.

R
Raşit Apalak
4 min read

ISR (Incremental Static Regeneration) gives you fast pages without rebuilding everything on every deploy. The missing piece is cache invalidation: when content changes in your CMS, your Next.js site should update quickly and reliably.

This guide explains the patterns that scale: webhook-triggered revalidation, tag-based invalidation, and safe, observable revalidation endpoints.


Table of Contents


When to use ISR vs SSR

Use ISR when:

  • content is mostly read-heavy (blogs, docs, marketing pages)
  • updates are occasional and can be handled by webhook-triggered refresh

Use SSR when:

  • content is user-specific (dashboards)
  • data changes constantly and must be real-time

Many sites mix both: ISR for content pages, SSR for authenticated app pages.


The core flow (CMS → webhook → revalidate)

The scalable flow looks like this:

  1. Editor publishes/updates content in the CMS
  2. CMS fires a webhook to your Next.js app
  3. Next.js handler calls revalidation for:
    • the changed page(s)
    • listing pages (blog index)
    • related pages (category/tag, sitemap)

You can revalidate:

  • a specific path (e.g. /blog/my-post)
  • a tag (e.g. posts, pages, navigation) if you use tag-based caching

Revalidation endpoint design (secure + simple)

Your revalidation route should be:

  • server-only
  • protected by a shared secret (REVALIDATE_SECRET)
  • strict about what it accepts (type + slug + locale)

Example payload your CMS webhook can send:

  • type: post | page | menu
  • slug: my-post
  • locale: en (optional)

Also include a signature or shared secret header/body field so random callers can’t trigger revalidation.


Revalidating pages vs tags

Revalidate paths

Best when you can compute the exact URLs to refresh:

  • /blog/[slug]
  • /pages/[slug]
  • /category/[slug]

Revalidate tags

Best when many pages depend on the same data:

  • all blog listing pages depend on the posts query
  • navigation depends on navigation

Tag-based invalidation is especially useful when one change affects many routes (e.g. menu update, global CTA).


What to revalidate for common content changes

Here are practical rules that prevent “stale edges”:

Post published/updated

  • the post page: /blog/<slug>
  • blog listing: /blog
  • category/tag pages if used
  • homepage “latest posts” if used
  • sitemap + RSS if you generate them dynamically

Post unpublished

  • same as above
  • plus: handle 404/redirect behavior if you remove content

Page updated (marketing/landing)

  • the page route: /<slug> or /pages/<slug>
  • any index pages that list pages

Navigation updated

  • revalidate the navigation tag (or any layouts that fetch navigation)

Observability: logs and retries

Webhook systems fail in real life. Make your revalidation observable:

  • Log webhook deliveries (type, slug, timestamp, result)
  • Return clear status codes (401 for bad secret, 400 for missing payload, 200 for success)
  • Support retries (CMS or your webhook relay)
  • Consider idempotency (multiple identical calls should be safe)

If you run multiple regions, keep the logic deterministic and avoid relying on in-memory state.


Pitfalls to avoid

  • Revalidating too little: listings stay stale even when the detail page updates.
  • Revalidating too much: nuking the entire cache for a single change becomes expensive.
  • No auth on revalidate endpoint: attackers can DoS your cache.
  • Webhook “created” vs “published” confusion: revalidate on publish events, not drafts.
  • Missing locale awareness: revalidate the locale-specific route(s).
Share this post:

Related posts