Quick answer
- Use Tag-Based Revalidation: In Next.js, fetch data with
next: { tags: ['post-123', 'blog-archive'] }instead of path-based revalidation. - Secure the Endpoint: Use a shared secret token in the
Authorizationheader between WordPress and Next.js. - Hook into WordPress: Use the
transition_post_statushook in WordPress to detect when content is published, updated, or deleted. - Send the Webhook: POST a payload containing the updated post ID, post type, and tags to your Next.js API route.
- Invalidate in Next.js: Call
revalidateTag()inside a Next.js App Router route handler to instantly clear the cache.
---
Why path-based revalidation fails in production
Many developers start with path-based revalidation (revalidatePath) because it looks simple. However, in a real-world headless WordPress setup, path-based revalidation quickly breaks down.
If you update a blog post, you do not just need to update /blog/my-post. You also need to update the /blog archive page, the category archives, the RSS feed, the search results page, and potentially the homepage featured section. Tracking down every single URL affected by a single post save is a maintenance nightmare.
Tag-based revalidation solves this. By tagging your Next.js fetch requests with granular identifiers (like post-123, post-type-post, or taxonomy-category-5), you can invalidate dozens of pages with a single API call. When a post changes, you tell Next.js to purge all caches associated with those specific tags. Next.js automatically rebuilds those pages on the next request.
Gotcha: Do not use the genericsave_posthook without checking the post status. Thesave_posthook fires multiple times during a single save operation (including revisions and auto-saves), which will flood your Next.js server with redundant revalidation requests.
---
Step 1: Configure Next.js fetch requests with tags
To use tag-based revalidation, you must assign tags when fetching data from the WordPress REST API or GraphQL endpoint in your Next.js application.
Here is how to structure your API fetch helper in the Next.js App Router. This example fetches a single post and tags it with both the specific post ID and the general post type.
// lib/wordpress.ts
export async function getPostBySlug(slug: string) {
const res = await fetch(`https://api.example.com/wp-json/wp/v2/posts?slug=${slug}`, {
next: {
tags: [`post-type-post`, `slug-${slug}`],
},
headers: {
'Content-Type': 'application/json',
},
});
if (!res.ok) {
throw new Error('Failed to fetch post');
}
const posts = await res.json();
return posts[0] || null;
}
When Next.js renders this page, it registers these tags in its data cache. Now, if you tell Next.js to revalidate post-type-post, every page that fetched data using that tag will have its cache cleared.
---
Step 2: Create the Next.js revalidation route handler
Next, create a secure API route handler in your Next.js App Router to receive the webhook from WordPress. This route validates the request using a shared secret token and executes the revalidation.
Create a file at app/api/revalidate/route.ts:
// app/api/revalidate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(request: NextRequest) {
const authHeader = request.headers.get('authorization');
const secret = process.env.REVALIDATION_SECRET;
if (!secret || authHeader !== `Bearer ${secret}`) {
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
}
try {
const body = await request.json();
const { tags } = body;
if (!tags || !Array.isArray(tags)) {
return NextResponse.json({ message: 'Missing tags array' }, { status: 400 });
}
tags.forEach((tag) => {
revalidateTag(tag);
});
return NextResponse.json({ revalidated: true, tags, now: Date.now() });
} catch (err) {
return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
}
}
Add your REVALIDATION_SECRET to your .env.local file. Generate a long, random string for this secret.
---
Step 3: Write the WordPress webhook trigger
To trigger the nextjs on-demand revalidation wordpress setup, you need to write a custom WordPress function that hooks into post status changes and sends a POST request to your Next.js API route.
Add this code to your custom plugin or your active theme's functions.php file. It uses transition_post_status to target only actual content changes, avoiding draft saves and revisions.
<?php
/**
* Trigger Next.js on-demand revalidation when content changes.
*/
add_action('transition_post_status', 'wp_nextjs_trigger_revalidation', 10, 3);
function wp_nextjs_trigger_revalidation($new_status, $old_status, $post) {
// Ignore auto-saves and revisions
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type === 'revision') {
return;
}
// Only trigger if the post is published, or was published and is now deleted/drafted
$is_published = $new_status === 'publish';
$was_published = $old_status === 'publish';
if (!$is_published && !$was_published) {
return;
}
// Define the tags to invalidate
$tags = [
'post-type-' . $post->post_type,
'slug-' . $post->post_name
];
// Send webhook to Next.js
wp_nextjs_send_revalidation_request($tags);
}
function wp_nextjs_send_revalidation_request($tags) {
$next_api_url = 'https://your-nextjs-site.com/api/revalidate';
$secret_token = 'YOUR_SHARED_SECRET_TOKEN'; // Match REVALIDATION_SECRET in Next.js env
$body = wp_json_encode(['tags' => $tags]);
wp_remote_post($next_api_url, [
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $secret_token,
],
'body' => $body,
'data_format' => 'body',
'blocking' => false, // Non-blocking request so WordPress editor doesn't lag
'timeout' => 5,
]);
}
Using 'blocking' => false is highly recommended. It ensures the WordPress editor remains fast and does not wait for the Next.js server to rebuild the cache before saving the post.
---
Step 4: Handle ACF fields and relationships
If you use Advanced Custom Fields (ACF) to build your pages, saving an ACF field group may not trigger the default post transition hooks correctly, or you might need to invalidate related pages. For example, if you update an ACF relationship field on a "Project" post, you need to revalidate the "Service" page that displays it.
To handle this, use the ACF acf/save_post hook. This hook runs after ACF has saved the metadata to the database, ensuring Next.js fetches the updated custom fields.
add_action('acf/save_post', 'wp_nextjs_acf_revalidation', 20);
function wp_nextjs_acf_revalidation($post_id) {
// Ensure it is a valid post ID, not options page
if (!is_numeric($post_id)) {
return;
}
$post = get_post($post_id);
if (!$post || $post->post_status !== 'publish') {
return;
}
$tags = [
'post-type-' . $post->post_type,
'slug-' . $post->post_name
];
// Example: If this post has a relationship field pointing to another page
$related_page_id = get_field('related_page', $post_id);
if ($related_page_id) {
$related_post = get_post($related_page_id);
if ($related_post) {
$tags[] = 'slug-' . $related_post->post_name;
}
}
wp_nextjs_send_revalidation_request($tags);
}
Ensure that "Show in REST API" is enabled on your ACF field groups so that your Next.js fetch requests can access the updated data under the acf key in the JSON response.
---
How to test and debug the revalidation flow
- Check WordPress HTTP Requests: Install a plugin like Query Monitor or Log HTTP Requests to verify that WordPress is firing the POST request to Next.js when you hit "Update".
- Inspect Next.js Server Logs: Monitor your Next.js console logs. If you see a
401 Unauthorizederror, double-check that yourREVALIDATION_SECRETenv variable exactly matches the token in your WordPress PHP file. - Verify Cache Status: Check the
x-nextjs-cacheresponse header on your frontend. It should change fromHITtoSTALEorMISSimmediately after you update the post in WordPress and refresh the frontend page.
---
Checklist
- Next.js fetch requests are configured with specific, granular tags.
- Shared secret token is generated and added to Next.js environment variables.
- WordPress webhook uses
transition_post_statusto avoid revision and auto-save triggers. - Webhook requests use
'blocking' => falseto protect WordPress editor performance. - ACF field groups have "Show in REST API" enabled.
- Webhook payload and Authorization headers are verified via a local testing tool or server logs.
- Next.js documentationNext.js documentation
- WordPress REST API HandbookWordPress REST API Handbook
- WPGraphQL docsWPGraphQL docs
Drafted with AI assistance. Code targets current WordPress, WooCommerce and Next.js APIs; test changes on a staging site before production.