How to Trigger Next.js ISR from a WordPress Webhook
On-demand ISR lets a Next.js site rebuild a single page the moment an editor publishes in WordPress, instead of waiting for a timed revalidation window or a full site rebuild. The mechanism is a signed webhook: WordPress fires a request to a Next.js API route on publish, the route verifies the request and calls revalidatePath or revalidateTag. Below is the exact setup, including the gotchas that break it in production.
What problem does on-demand ISR actually solve?
Static Generation (SSG) is fast but frozen at build time. A fixed revalidate interval (say, 60 seconds) fixes staleness but wastes rebuilds on pages nobody changed, and still leaves up to a minute of stale content after a real edit. Neither is good enough for a WordPress-backed site where editors expect "publish" to mean "live now."
On-demand revalidation solves this by making WordPress tell Next.js exactly when and what changed. No polling, no wasted rebuilds, no fixed staleness window. The trade-off is that you now own a small integration: a webhook sender in WordPress and a webhook receiver in Next.js.
How do you fire a webhook from WordPress on publish?
Hook into transition_post_status rather than save_post. save_post fires on every autosave and draft save, which means an unfiltered handler will hammer your Next.js endpoint constantly while an editor is still writing. transition_post_status lets you catch the exact moment a post moves into publish:
add_action('transition_post_status', function ($new_status, $old_status, $post) {
if ($new_status !== 'publish' || $old_status === 'publish') {
return; // only fire on the actual publish transition
}
if (wp_is_post_autosave($post) || wp_is_post_revision($post)) {
return;
}
$payload = wp_json_encode([
'slug' => $post->post_name,
'type' => $post->post_type,
]);
$signature = hash_hmac('sha256', $payload, WP_REVALIDATE_SECRET);
wp_remote_post('https://yourdomain.com/api/revalidate', [
'headers' => [
'Content-Type' => 'application/json',
'X-Revalidate-Signature' => $signature,
],
'body' => $payload,
'timeout' => 5,
'blocking' => false, // don't make the editor wait on the publish button
]);
}, 10, 3);
The signature matters more than it looks. Without it, your revalidation endpoint is a public POST route that anyone can hit to force rebuilds on demand — see the mistakes section below.
How do you build the Next.js route that receives it?
An App Router route handler verifies the HMAC signature, then calls the appropriate revalidatePath or revalidateTag function:
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get('x-revalidate-signature');
const expected = crypto
.createHmac('sha256', process.env.REVALIDATE_SECRET!)
.update(body)
.digest('hex');
if (signature !== expected) {
return NextResponse.json({ revalidated: false }, { status: 401 });
}
const { slug, type } = JSON.parse(body);
revalidatePath(`/blog/${slug}`);
revalidateTag(type);
return NextResponse.json({ revalidated: true, now: Date.now() });
}
Two calls, not one, on purpose. revalidatePath rebuilds the exact page that changed. revalidateTag catches everything else that references it — the blog index, a category archive, a related-posts widget — as long as those fetches were tagged when they were made.
Path-based vs tag-based — which do you need?
| Path-based (revalidatePath) | Tag-based (revalidateTag) | |
|---|---|---|
| Best for | One URL changed, you know the exact path | One update touches many pages — an author bio, a category, a shared widget |
| Setup cost | None extra — call it with the path | Every relevant fetch needs next: { tags: [...] } when the data is first requested |
| Precision | Exact — only that route rebuilds | Broad by design — can over-trigger if tags are too coarse |
| Typical use here | A single lesson page going live | A category archive and its related-content widget after a tag change |
Most posts only need path-based revalidation. Reach for tags when one edit is known to affect more than one rendered page.
What did this look like in production on AzanGuru?
This is the exact pattern behind AzanGuru's content pipeline. An editor publishes a lesson in WordPress; the webhook fires; Next.js revalidates just that lesson's path. Measured end-to-end, that round trip averages 3-8 seconds. A naive full-site SSG rebuild at AzanGuru's current catalogue size takes roughly 45 seconds — and would mean every single page rebuilds because one lesson changed, not just the one that actually did.
The gap between those two numbers is the entire argument for wiring this up instead of relying on a fixed revalidate interval or a full redeploy per edit.
Common mistakes
- Skipping the signature check. A public, unauthenticated revalidation endpoint lets anyone force rebuilds on a schedule of their choosing — wasted build minutes at best, a crude denial-of-service vector at worst.
- Hooking `save_post` instead of `transition_post_status`. `save_post` fires on autosave and every draft save. Without a status check you'll fire the webhook dozens of times while a post is still being written.
- Forgetting `blocking => false`. A blocking `wp_remote_post` call makes the WordPress admin wait on Next.js's response before the publish action finishes. Fire it async.
- Assuming every content change fires the hook. Programmatic updates — a `wp post meta update` from WP-CLI, or a migration script writing straight to the database — do not fire `save_post` or `transition_post_status`. If content changes outside the normal editor flow, the webhook has to be triggered manually or the page goes stale silently.
- Revalidating the post but not the pages that list it. The single post often isn't the only place that content appears — remember the index, the category archive, and any "latest" widget.
Frequently asked questions
What is on-demand ISR in Next.js?
On-demand ISR (Incremental Static Regeneration) lets you rebuild one specific statically generated page on request, instead of waiting for a fixed revalidation timer or triggering a full site rebuild. You call revalidatePath or revalidateTag from a server context — typically an API route hit by a webhook — and Next.js regenerates just that page in the background.
Do I need a plugin to trigger revalidation from WordPress?
No. A single add_action hook on transition_post_status in your theme's functions.php or a small must-use plugin is enough — no third-party webhook plugin required, though one can simplify things if you're managing many trigger conditions.
How is this different from a fixed revalidate interval in getStaticProps or fetch?
A fixed interval (e.g. revalidate: 60) regenerates a page the next time it's requested after 60 seconds have passed, whether or not the content actually changed. On-demand revalidation only fires when WordPress tells it something changed, so there's no wasted rebuild and no up-to-a-minute staleness window after a real edit.
Is it safe to expose a public revalidation API route?
Only with signature verification. Without it, the route is a public POST endpoint anyone can call to force rebuilds. Sign the WordPress request with an HMAC secret shared between both sides, and reject any request whose signature doesn't match.
What happens if the webhook request fails?
The page stays on its last built version until the next successful trigger — a fixed revalidate interval as a fallback, or a manual rebuild. It fails safe: visitors see stale-but-valid content, never a broken page. Worth logging failed webhook deliveries so a silent miss doesn't go unnoticed for weeks.
Does this work with the WordPress REST API or only WPGraphQL?
Either. The webhook only needs to tell Next.js which slug and post type changed — it doesn't fetch content itself. Next.js re-fetches the actual data from whichever API (REST or WPGraphQL) it already uses once revalidation runs.
How fast does revalidation actually happen in production?
On AzanGuru this averages 3-8 seconds from WordPress publish to the regenerated page being live, versus roughly 45 seconds for a naive full-catalogue rebuild. Actual timing depends on page complexity and how much data the page fetches on regeneration.
Need this wired into your stack?
Getting webhook-triggered ISR right the first time saves weeks of chasing stale-content bugs later. If you're building a headless WordPress site on Next.js and want this set up correctly, get in touch.