Next.js revalidate
WordPress Plugin to revalidate Next.js pages
by superhuit · github.com/superhuit-agency/nextjs-revalidate · website
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/superhuit-agency/nextjs-revalidate/releases/download/v1.6.9/nextjs-revalidate-v1.6.9.zipReadme
Next.js revalidate
Next.js plugin allows you to purge & re-build the cached pages from the WordPress admin area. It also automatically purges & re-builds when a page/post/... is saved or updated.
The revalidation request is sent to an endpoint composed from the settings — the revalidate domain joined to the revalidate path — with two query arguments.
- The relative
pathto revalidate - The
secretto protect the revalidation endpoint.
Settings
| Setting | Required | Default |
|---|---|---|
| Revalidate domain | yes | — (e.g. https://example.com) |
| Revalidate path | no | /api/revalidate |
| FSE revalidate path | no | /api/revalidate-fse |
| Revalidate secret | yes | — |
| Revalidate on FSE update | no | on for a new install, off for an upgrade |
A standard install fills in the domain and the secret. The paths exist for apps that route these endpoints somewhere else; each falls back to the default shown in its placeholder when left empty.
FSE templates
Next.js renders its pages inside WordPress FSE templates, and holds the whole template structure as one cached value. Saving a template or a template part in the site editor, resetting one to its theme default, or switching themes therefore changes every page at once — so the plugin sends one request to the FSE endpoint, with the secret and no path, and the front-end's pages rebuild lazily from there. Nothing is enqueued and nothing is rebuilt page by page.
https://example.com/api/revalidate-fse?secret=my-super-secret-string
Menu changes do not trigger this: menu items are fetched at request time by the front-end and are not part of the template snapshot.
Revalidate on FSE update starts on for a new install and off for a site upgrading from an earlier release — an existing front-end may not serve that endpoint yet, and every template save would otherwise ask it for a route it does not have. Switch it on once the front-end is serving it.
Sites upgrading from 1.6.x had a single, fully-qualified revalidate URL. It is split into a domain and a path automatically on the first admin request after the upgrade, custom paths and all — nothing to do by hand.
Example
https://example.com/api/revalidate?path=/hello-world/&secret=my-super-secret-string
Based on the Next.js On-demand revalidation documentation
Probing the front-end
The Probe tab of the settings screen asks the front-end to rebuild one path straight away, and shows what it answered — including the error message and its code when it did not work. It is a real revalidation and not a dry run: the page is rebuilt exactly as it would be after an edit, using the saved settings, so it answers "does this site revalidate right now" rather than "would these values work".
A probe is never counted towards the "not keeping this site up to date" warning:
pressing it can neither raise that warning nor clear it. It is written to the log
file when logging is on, marked 🔎 Probe.
Requirements
- Requires PHP 7.4+
- Requires WordPress 5.0+
API functions
Neither function can tell you whether the front-end has rebuilt anything. Each
answers what the plugin took on: nextjs_revalidate_purge_url whether the
revalidation was accepted into the queue, and
nextjs_revalidate_schedule_purge_url whether the schedule was registered —
which is one step further away, since that revalidation is only enqueued when
the date time passes, and can be refused then. The queue is drained afterwards,
by cron, so there is no return value in this plugin that could report the
outcome of a delivery that has not happened yet. Delivery is at most once — a
revalidation that is attempted and fails is written to the log and dropped,
never retried.
nextjs_revalidate_purge_url
Enqueues a revalidation of any URL, to be delivered to the front-end by the next run of the queue's cron.
Usage
nextjs_revalidate_purge_url( $url );
Arguments
| Name | Type | Description |
|---|---|---|
| url | string | The URL to purge |
| priority | int | Optional. Lower numbers are purged earlier; equal priorities keep insertion order. Default 10. |
Returns
bool — whether the revalidation was accepted into the queue. It is false
when the site is unconfigured, which is a refusal: the revalidate domain or
the secret is missing, nothing has been queued, and nothing will be. It is also
false if the queue insert failed. A URL already waiting in the queue is
accepted (true) without being queued twice.
It is never a statement about the front-end. A true says the plugin will try.
nextjs_revalidate_schedule_purge_url
Registers a revalidation of the given URL for a future date time. Nothing is enqueued until then, so a schedule registered on a configured site is still refused at its due time if the site is unconfigured by then.
Usage
nextjs_revalidate_schedule_purge_url( $datetime, $url );
Arguments
| Name | Type | Description |
|---|---|---|
| datetime | string | The date time when to purge |
| url | string | The URL to purge |
Returns
bool — whether this call registered the scheduled purge. It is false when
that URL is already registered for that date time: the schedule stands, and this
call added nothing to it. It is also false if the write failed.
Which posts are revalidated
A post is revalidated when the front-end could hold a page for it: its post type
is viewable — WordPress's own publicly_queryable test, via
is_post_type_viewable()
— and its status is publish or private, or it has just left publish for
draft or trash. Posts of a post type that is not viewable are never
revalidated, whatever their status.
A headless site registering post types with publicly_queryable => false while
its front-end still renders their permalinks can say so with the filter below.
Integrations
An integration is a third-party plugin whose changes this plugin reacts to when that plugin is present. It is never a dependency: with the plugin absent nothing registers, and no feature here requires one.
Redirection
A headless front-end resolves a redirect inside the cached page of the path it redirects from, so creating, editing, deleting, enabling or disabling a redirect in Redirection leaves that path answering as it did before until its cache entry expires. With Redirection active, this plugin enqueues a revalidation of the source path whenever a redirect changes, so the redirect starts — or stops — working within the time the queue takes to drain.
Only a redirect the front-end could resolve for a single path produces a revalidation: its source is a literal path rather than a regular expression, and it is enabled. A regular expression source matches an unbounded set of paths, so there is no single path to rebuild; it is skipped, and the skip is written to the log when logging is switched on. Source paths are reduced to their path component, dropping any query string or domain the source was stored with, and given the site's trailing slash convention, so they match the form post permalinks are enqueued in.
Changing a redirect's source revalidates the path it stopped redirecting as well as the new one, on Redirection versions whose update action carries the redirect's previous state. Redirection 5.9.0 passes the redirect's id instead, where only the new source path is revalidated.
Filters
nextjs_revalidate_should_revalidate_redirect
Filters whether a redirect's source path is revalidated. Return false to leave
the path alone — the escape hatch for a site whose front-end resolves redirects
some other way.
Usage
add_filter( 'nextjs_revalidate_should_revalidate_redirect', function( $should_revalidate, $path, $redirect ) {
if ( 0 === strpos( $path, '/legacy/' ) ) return false;
return $should_revalidate;
}, 10, 3 );
Arguments
| Name | Type | Description |
|---|---|---|
| should_revalidate | bool | Whether the source path is revalidated |
| path | string | The source path, normalised |
| redirect | object | The redirect the path is the source of, as Redirection's own Red_Item |
nextjs_revalidate_purge_should_revalidate_post_on_save
Filters whether the given post is revalidated. Applied last, so it can admit a post the rules above decline, or decline one they admit.
Usage
add_filter( 'nextjs_revalidate_purge_should_revalidate_post_on_save', function( $should_revalidate, $post_id ) {
if ( 'my-headless-type' === get_post_type( $post_id ) ) return 'publish' === get_post_status( $post_id );
return $should_revalidate;
}, 10, 2 );
Arguments
| Name | Type | Description |
|---|---|---|
| should_revalidate | bool | Whether the post is revalidated |
| post_id | int | The post ID |
nextjs_revalidate_purge_action_permalink
Filters the permalink added to the purge queue by the "Purge cache" row and bulk
actions. Return false to keep it out of the queue.
Arguments
| Name | Type | Description |
|---|---|---|
| permalink | string|false | The post permalink. False if the post is not revalidatable |
| post_id | int | The post ID |
Tests
Two commands, and which one a test belongs to depends on whether it needs real
WordPress state. See docs/adr/0008-two-testing-idioms.md. A third idiom is not
a command at all — see the runbook below.
npm run test:php — standalone scripts
Scripts under tests/ that stub the handful of WordPress functions their
subject touches. No framework, no database, no Docker; they run anywhere PHP
does, including a sandbox with neither.
npm run test:integration — the integration suite
PHPUnit tests under tests/integration/ that boot WordPress with this plugin
active and assert on the revalidation queue: given some WordPress state and
an event, which paths does the queue revalidate, in what order, at what
priority?
From a fresh checkout:
npm install
composer install
npm run test:integration
wp-env installs the Redirection plugin alongside this one, in both environments, so the redirect integration can be exercised without assembling an install by hand. The suite's bootstrap loads it and creates its tables when it is there, and skips the tests that need it when it is not.
The command starts wp-env itself — wp-env start is idempotent, so running it
again costs seconds. It runs with --no-scripts and against the tests
environment, so the development site keeps its database and its settings;
wp-env does bring the development containers up alongside the tests ones, which
means port 8080 has to be free. Docker must be running. The first run downloads
WordPress and its PHPUnit test library and takes a few minutes.
Write a test by extending NextJsRevalidate\Tests\QueueTestCase, which
configures the site, enqueues paths and reads the queue back:
$this->configure_site(); // a configured site — an
// unconfigured one refuses
$this->enqueue( '/hello-world/', 5 ); // enqueue a path at a priority
$this->assertQueueRevalidates( [ '/hello-world/' ] ); // paths, in drain order
$this->assertQueueRevalidatesAtPriorities( [ '/hello-world/' => 5 ] );
$this->assertQueueHolds( [ home_url( '/hello-world/' ) ] ); // the permalinks
The queue holds permalinks, and those permalinks revalidate paths — the
two are kept apart because on a network they can disagree. assertQueueHolds()
takes permalinks; assertQueueRevalidates() takes paths and normalises against
the site's home url, so a test survives a change of testsPort.
The queue table is created once in the bootstrap and emptied around every test.
It has to be: RevalidateQueue::add_item() runs its own transaction, whose
COMMIT also commits the one WP_UnitTestCase uses to roll a test back.
The manual test runbook — checks no command can run
An admin notice rendering on the screen it is meant for, a redirect saved through
Redirection's own UI, a site upgraded from 1.6.9. What puts a check here is
reach — whether the answer can only come from a browser, a console or a file
on a running site — never its subject. See
docs/adr/0012-a-third-testing-idiom.md.
Two files, partitioning the checks between them. No step appears in both.
docs/manual-tests.md— the core pass: steps on a single site. Run it before every release, and after any change worth ten minutes.docs/manual-tests-extended.md— the extended pass: everything else, including the network stack and a site upgraded from the previous release. Run the part covering what you touched, and all of it before a structural release.
Both are committed unchecked. Tick the boxes in your working copy during a pass; never commit the ticks.
Read the full README on GitHub →
Releases
| Tag | Published | Asset | Downloads |
|---|---|---|---|
| v1.6.9 | Apr 28, 2026 | nextjs-revalidate-v1.6.9.zip | 1 |
| v1.6.8 | Feb 5, 2026 | nextjs-revalidate-v1.6.8.zip | 235 |
| v1.6.6 | Oct 20, 2025 | nextjs-revalidate-v1.6.6.zip | 5 |
| v1.6.5 | Jul 5, 2024 | nextjs-revalidate-v1.6.5.zip | 514 |
| v1.6.4 | Apr 25, 2024 | nextjs-revalidate-v1.6.4.zip | 944 |
| v1.6.3 | Feb 14, 2024 | nextjs-revalidate.zip | 39 |
| v1.6.2 | Feb 6, 2024 | nextjs-revalidate.zip | 3 |
| v1.6.1 | Feb 5, 2024 | nextjs-revalidate.zip | 2 |
| v1.6.0 | Jan 26, 2024 | nextjs-revalidate.zip | 2 |
| v1.5.0 | Jan 9, 2024 | nextjs-revalidate.zip | 4 |
| v1.4.1 | Nov 23, 2023 | nextjs-revalidate.zip | 5 |
| v1.4.0 | Nov 15, 2023 | nextjs-revalidate.zip | 4 |
| v1.3.0 | Sep 12, 2023 | nextjs-revalidate.zip | 110 |
| v1.2.2 | May 23, 2023 | nextjs-revalidate.zip | 170 |
| v1.2.1 | May 17, 2023 | nextjs-revalidate.zip | 105 |
| v1.2.0 | May 15, 2023 | nextjs-revalidate.zip | 4 |
| v1.1.3 | Apr 27, 2023 | nextjs-revalidate.zip | 4 |
| v1.1.2 | Apr 27, 2023 | nextjs-revalidate.zip | 4 |
| v1.1.1 | Apr 20, 2023 | nextjs-revalidate.zip | 4 |
| v1.1.0 | Feb 24, 2023 | nextjs-revalidate.zip | 122 |
| v1.0.2 | Feb 17, 2023 | nextjs-revalidate.zip | 4 |
| v1.0.1 | Dec 27, 2022 | nextjs-revalidate.zip | 396 |
| v1.0.0 | Dec 26, 2022 | nextjs-revalidate.zip | 9 |
Active-site estimate ≈240 comes from the median of recent superseded releases. Method.