On-Demand ISR — Triggering Revalidation via API Routes
In this tutorial, you will learn about On. We cover key concepts, practical examples, and best practices to help you master this topic.
On-demand ISR triggers page revalidation instantly via API routes, enabling immediate content updates when CMS publishes or edits content.
What You'll Learn
By the end of this tutorial, you'll understand how on-demand ISR works, how to create revalidation API endpoints, how to secure them with secrets, and how to integrate with CMS Webhooks.
Why It Matters
Time-based revalidation has a delay window. If a content editor publishes an article, it won't appear until the revalidate window expires. On-demand ISR eliminates this delay by triggering revalidation the moment content changes.
Real-World Use
A CMS editor publishes a new blog post. The CMS sends a Webhook to the revalidation API. The new post page and the blog listing page regenerate immediately. Readers see the new content within seconds of publishing.
On-Demand ISR Flow
graph TD
A[Content Editor
publishes in CMS] --> B[CMS Webhook
POST to /api/revalidate]
B --> C[Verify
webhook secret]
C --> D{Valid secret?}
D -->|No| E[Return 401]
D -->|Yes| F[res.revalidate
specific paths]
F --> G[Revalidate
post page]
F --> H[Revalidate
listing pages]
G --> I[New static HTML
generated]
H --> I
I --> J[CDN cache updated]
J --> K[Users see
fresh content]
style A fill:#27ae60,color:#fff
style B fill:#4a90d9,color:#fff
style F fill:#e67e22,color:#fff
style J fill:#27ae60,color:#fff
Revalidation API Endpoint
// pages/api/revalidate.js — On-demand revalidation
export default async function handler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
// Verify webhook secret
const { secret, paths } = req.body;
if (secret !== process.env.REVALIDATION_TOKEN) {
return res.status(401).json({ message: 'Invalid secret' });
}
if (!paths || !Array.isArray(paths) || paths.length === 0) {
return res.status(400).json({ message: 'Paths array required' });
}
try {
// Revalidate all specified paths in parallel
const results = await Promise.allSettled(
paths.map(path => res.revalidate(path))
);
const succeeded = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.error('Revalidation failures:', failed.map(f => f.reason));
}
return res.json({
revalidated: true,
succeeded,
failed: failed.length,
errors: failed.map(f => f.reason.message)
});
} catch (err) {
console.error('Revalidation error:', err);
return res.status(500).json({
message: 'Error revalidating',
error: err.message
});
}
}
CMS Webhook Integration
// Contentful webhook handler
export async function handleContentfulWebhook(secret) {
return async function handler(req, res) {
if (req.query.secret !== secret) {
return res.status(401).json({ message: 'Invalid secret' });
}
const { fields, sys } = req.body;
const contentType = sys.contentType.sys.id;
const slug = fields?.slug?.['en-US'];
const pathsToRevalidate = [];
if (slug) {
pathsToRevalidate.push(`/blog/${slug}`);
}
// Always revalidate listing pages
pathsToRevalidate.push('/blog');
pathsToRevalidate.push('/');
// Revalidate category pages if applicable
if (fields?.category) {
const category = fields.category['en-US'];
pathsToRevalidate.push(`/category/${category}`);
}
try {
await Promise.all(
pathsToRevalidate.map(path => res.revalidate(path))
);
res.json({ revalidated: true, paths: pathsToRevalidate });
} catch (err) {
res.status(500).json({ error: err.message });
}
};
}
// Sanity webhook handler
export async function handleSanityWebhook(req, res) {
const signature = req.headers['sanity-signature'];
const isValid = verifySanitySignature(signature, req.body);
if (!isValid) {
return res.status(401).json({ message: 'Invalid signature' });
}
const { slug, _type } = req.body;
const paths = ['/'];
if (_type === 'post') {
paths.push(`/blog/${slug}`);
paths.push('/blog');
} else if (_type === 'product') {
paths.push(`/products/${slug}`);
paths.push('/products');
}
await Promise.all(paths.map(path => res.revalidate(path)));
return res.json({ revalidated: true });
}
Serverless Webhook Handler
// pages/api/webhook.js — Generic webhook for multiple CMS
export default async function handler(req, res) {
const { secret, source } = req.query;
if (secret !== process.env.REVALIDATION_TOKEN) {
return res.status(401).json({ message: 'Invalid token' });
}
// Parse webhook payload based on source
let paths = [];
const payload = req.body;
switch (source) {
case 'contentful':
paths = parseContentfulPayload(payload);
break;
case 'sanity':
paths = parseSanityPayload(payload);
break;
case 'strapi':
paths = parseStrapiPayload(payload);
break;
case 'strapi':
paths = parseStrapiPayload(payload);
break;
case 'webhook':
paths = parseCustomWebhook(payload);
break;
default:
return res.status(400).json({ message: 'Unknown source' });
}
// Include listing pages
paths.push('/');
try {
await Promise.all(paths.map(path => res.revalidate(path)));
res.json({
revalidated: true,
paths,
timestamp: Date.now()
});
} catch (err) {
res.status(500).json({ error: err.message });
}
}
function parseContentfulPayload(body) {
const paths = [];
const slug = body?.fields?.slug?.['en-US'];
if (slug) paths.push(`/blog/${slug}`);
paths.push('/blog');
return paths;
}
function parseSanityPayload(body) {
const paths = [];
const slug = body?.slug?.current;
if (slug && body?._type === 'post') paths.push(`/blog/${slug}`);
paths.push('/blog');
return paths;
}
function parseStrapiPayload(body) {
const paths = [];
const slug = body?.entry?.slug;
if (slug) paths.push(`/blog/${slug}`);
paths.push('/blog');
return paths;
}
Multiple Secret Management
// pages/api/revalidate.js — Multi-tenant revalidation
const SITES = {
blog: process.env.BLOG_REVALIDATION_TOKEN,
docs: process.env.DOCS_REVALIDATION_TOKEN,
shop: process.env.SHOP_REVALIDATION_TOKEN
};
export default async function handler(req, res) {
const { secret, site, paths } = req.body;
// Verify secret for the specific site
if (secret !== SITES[site]) {
return res.status(401).json({ message: 'Invalid secret' });
}
try {
await Promise.all(paths.map(path => res.revalidate(path)));
res.json({
revalidated: true,
site,
paths,
time: Date.now()
});
} catch (err) {
console.error(`Revalidation failed for ${site}:`, err);
res.status(500).json({ error: err.message });
}
}
Common Mistakes
- Exposing the revalidation endpoint without authentication. Anyone who finds your webhook URL can trigger rebuilds. Use a strong random secret token.
- Not validating the webhook payload. Always verify the webhook signature or secret. Unauthorized requests can trigger expensive rebuilds.
- Revalidating every path on every change. If a single page changes, only revalidate that page and its listing. Don't revalidate the entire site.
- Not handling concurrent revalidation requests. Multiple webhooks arriving simultaneously can overload the server. Queue or deduplicate requests.
- Forgetting to revalidate listing pages. When a new post is published, both the post page and the blog listing need revalidation.
Practice Questions
- How does on-demand ISR differ from time-based revalidation?
- How do you secure a revalidation API endpoint?
- What paths should you revalidate when a new blog post is published?
- How do you integrate on-demand ISR with a headless CMS webhook?
- How do you handle revalidation of multiple pages at once?
Challenge: Set up a complete on-demand ISR pipeline: create a revalidation API endpoint with secret authentication, configure a Contentful webhook to POST to your endpoint, verify that content updates appear within seconds, and add error logging.
FAQ
Mini Project
Create a CMS-integrated blog with on-demand ISR: set up a revalidation API endpoint, configure a webhook simulator that sends mock CMS payloads, implement path-based revalidation for posts and listings, add error monitoring, and verify content updates appear within 2 seconds.
What's Next
You've mastered on-demand revalidation. Now explore the Stale-While-Revalidate Pattern in depth to understand how ISR serves content during revalidation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro