One of the biggest challenges I’ve faced when scaling static sites is the ‘stale data’ problem. Standard Incremental Static Regeneration (ISR) is great, but relying on a 60-second or 1-hour timer means your users are often looking at outdated content. When you’re managing a high-traffic blog or an e-commerce store, waiting for a timer is unacceptable.
That’s where on-demand ISR comes in. Instead of telling Next.js to check for updates every X seconds, you tell it exactly when to update. In this tutorial, I’ll show you exactly how to implement on-demand ISR in Next.js so your content updates the millisecond you hit ‘Save’ in your CMS.
Prerequisites
Before we dive into the code, make sure you have the following set up:
- A Next.js project (version 13+ using the App Router is recommended, though this works with the Pages Router too).
- A headless CMS or a database (like Contentful, Strapi, or Sanity) that supports webhooks.
- A basic understanding of JAMstack architecture best practices to understand why we’re choosing static generation over SSR.
Step 1: Configure Your Static Page
First, we need a page that is statically generated. In the App Router, pages are static by default unless you use dynamic functions. I usually set up my product or blog pages like this:
// app/posts/[slug]/page.tsx
import { getPostBySlug } from '@/lib/api';
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Step 2: Create the Revalidation API Route
To trigger the update, we need a secure endpoint that our CMS can call via a webhook. This is the core of how to implement on-demand ISR in Next.js. We’ll use a Route Handler in the App Router.
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
// 1. Security Check: Prevent unauthorized revalidation
if (secret !== process.env.MY_SECRET_TOKEN) {
return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
}
try {
const body = await request.json();
const slug = body.slug; // Assuming CMS sends the slug of the changed post
// 2. Revalidate the specific path
revalidatePath(`/posts/${slug}`);
// Optional: Revalidate a tag if you're using fetch tags
// revalidateTag('posts');
return NextResponse.json({ revalidated: true, now: Date.now() });
} catch (err) {
return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
}
}
As shown in the image below, the logic flow involves a secure handshake between your CMS and this endpoint to ensure random users can’t trigger expensive rebuilds of your site.
Step 3: Setting Up the CMS Webhook
Now that the endpoint exists, you need to tell your CMS to call it. In my experience, this is where most developers get stuck. Here is the general workflow:
- Go to your CMS settings → Webhooks.
- Set the URL to
https://yourdomain.com/api/revalidate?secret=your_token_here. - Select the trigger event (e.g., “Post Published” or “Post Updated”).
- Ensure the payload is sent as JSON and contains the
slugof the post.
If you’re integrating this with a headless WordPress with Next.js guide, you might use a plugin like WP Webhooks to handle this trigger.
Pro Tips for Production
1. Use revalidateTag for Bulk Updates
If you update a category name that affects 100 posts, calling revalidatePath 100 times is inefficient. Instead, use revalidateTag. When fetching data, add a tag to your request:
fetch('https://api.example.com/posts', { next: { tags: ['posts'] } })
Then, in your API route, simply call revalidateTag('posts') to purge the entire cache for those requests.
2. Handle Race Conditions
I’ve found that some CMSs send multiple webhooks for a single save. To prevent unnecessary load, consider adding a small debounce or a check to see if the content has actually changed before calling the revalidation function.
Troubleshooting Common Issues
| Issue | Likely Cause | Solution |
|---|---|---|
| 401 Unauthorized | Token mismatch | Check your .env and the webhook URL query string. |
| Page doesn’t update | Caching layer (CDN) | Check if you have a CDN like Cloudflare in front of Vercel that needs its own purge. |
| 500 Internal Error | Payload mismatch | Log request.json() to ensure the CMS is sending the slug as expected. |
What’s Next?
Now that you’ve mastered on-demand ISR, you might be wondering if you’re using the right framework for your specific scale. If you find that your site is purely static with very rare updates, you might want to explore Astro vs Next.js for static sites to see if a different architecture would better serve your needs.
Ready to optimize your site? Start by implementing the revalidation route today and stop relying on timers!