@nextjscms/plugin-cache-revalidation
v2.3.1
Published
Readme
@nextjscms/plugin-cache-revalidation
Keeps a cached Next.js front-end in sync with the CMS, and shows you when it didn't.
Built for sites that cache aggressively — 'use cache' with cacheTag() and a long
cacheLife profile. Content published in the CMS is invisible to visitors until
something purges the matching tag, and if that purge quietly fails there is nothing
to tell you. This plugin sends the purge, retries it, records what never landed, and
gives you a page to fix it from.
What the site must provide
A route that accepts the purge and calls revalidateTag. Roughly:
// app/api/revalidate/route.ts (on the public site)
import { revalidateTag } from 'next/cache'
export async function POST(req: Request) {
if (req.headers.get('x-revalidate-secret') !== process.env.REVALIDATE_SECRET) {
return Response.json({ revalidated: false }, { status: 401 })
}
const body = await req.json()
const changes = Array.isArray(body) ? body : [body]
for (const change of changes) {
revalidateTag(change.type, { expire: 0 })
if (change.id != null) revalidateTag(`${change.type}:${change.id}`, { expire: 0 })
}
return Response.json({ revalidated: true })
}The plugin POSTs { type, id? } (or an array of them) with an x-revalidate-secret
header. { expire: 0 } is the documented pattern for external webhooks; a 4xx from
this route is treated as permanent, a 5xx or network error as retryable.
Install
pnpm add @nextjscms/plugin-cache-revalidation// cms.config.ts
import { cacheRevalidationPlugin } from '@nextjscms/plugin-cache-revalidation'
export default defineConfig({
plugins: [
{
plugin: cacheRevalidationPlugin,
options: {
// The cache tags your site uses — must match its cacheTag() calls.
contentTypes: ['posts', 'categories', 'tags', 'about'],
},
},
],
})Register the server entrypoint alongside your other plugins:
// app/(rootLayout)/(plugins)/[...slug]/plugin-server-registry.ts
export const pluginNamesMap: Record<string, string> = {
plugin_cache_revalidation: '@nextjscms/plugin-cache-revalidation/server',
}Make sure your globals.css scans plugin output, or the page will render unstyled:
@source '../node_modules/@nextjscms/plugin-*/**/*.{tsx,ts,jsx,js}';Environment
| Variable | Required | Meaning |
| ------------------- | -------- | ----------------------------------------------- |
| REVALIDATE_URL | yes | Origin of the public site, e.g. https://x.com |
| REVALIDATE_SECRET | yes | Shared secret; must match the site's |
Rename them with the urlEnv / secretEnv options if they clash with something.
Revalidating on content changes
Add the hooks to any section whose content the site caches. The argument is the cache tag for that section.
// sections/posts.section.ts
import { siteRevalidationHooks } from '@nextjscms/plugin-cache-revalidation/hooks'
export default hasItemsSection({
// …
hooks: siteRevalidationHooks('posts'),
})Merging with hooks you already have:
hooks: {
...siteRevalidationHooks('posts'),
beforeCreate: async (item) => { /* … */ },
}Pass includeEntityId: false for sections whose row ids are not what the site tags —
a singleton page, or a taxonomy whose ids would collide with another section's entity
tags:
hooks: siteRevalidationHooks('about', { includeEntityId: false })The hooks fire on create, update, and delete, including non-default locale submits.
Work is scheduled with after(), so a slow or unreachable site never delays a save,
and a failed purge never rolls back a mutation that already succeeded.
The page
Mounted at /cache-revalidation. Shows what is currently stale, grouped by type,
with the most recent failures and why each one failed. Three actions:
- Retry — re-sends every recoverable failure as one batched request, and clears only the rows that actually purged.
- Dismiss — clears failures that can never succeed (a rejected secret, an unknown tag). Retrying those is pointless; the fix is in your configuration.
- Purge — emergency escape hatch. Drops the cache for one content type or all of them, whether or not anything failed.
To use it as the dashboard:
dashboard: {
override: '/cache-revalidation'
}Translations
The plugin ships English and Arabic, but keeps them in the plugin rather than in the CMS base dictionary — installs without this plugin shouldn't carry its strings.
Merging them is required. The UI resolves through the CMS dictionary store like everything else, so without the merge it renders raw key names:
// cms.config.ts
import { pluginDictionaries } from '@nextjscms/plugin-cache-revalidation/translations'
i18n: {
supportedLanguages: {
en: { ...en, ...pluginDictionaries.en },
ar: { ...ar, ...pluginDictionaries.ar },
},
fallbackLanguage: 'en',
}Your entries win over the plugin's, so overriding a single string is just:
ar: { ...ar, ...pluginDictionaries.ar, revalidationTitle: 'الذاكرة المؤقتة' },Another language works the same way — spread pluginDictionaries.en first so any
key you haven't translated falls back to English instead of showing its key name.
If you re-type useI18n against your merged dictionary, the plugin's keys become
type-safe in your own components too:
// lib/translations/use-i18n.ts
import type en from './en'
export function useI18n() {
return useBaseI18n() as unknown as (key: keyof typeof en, params?: Record<string, string | number>) => string
}Options
| Option | Default | Notes |
| ------------------- | ------------------- | ------------------------------------------------ |
| contentTypes | — | Required. The cache tags your site uses. |
| urlEnv | REVALIDATE_URL | Env var holding the site origin. |
| secretEnv | REVALIDATE_SECRET | Env var holding the shared secret. |
| path | /api/revalidate | Path of the site's revalidate route. |
| timeoutMs | 3000 | Per-attempt timeout. |
| attempts | 2 | Total attempts for retryable failures. |
| skipInDevelopment | false | Send nothing unless NODE_ENV === 'production'. |
skipInDevelopment is worth turning on when one REVALIDATE_URL is shared across
environments and you don't want local edits purging the live site. It is off by
default because a plugin that silently does nothing is worse than one that does what
it says.
Storage
Failures are recorded in a revalidation_failures table, created automatically on
first use — there is no migration to run. Rows are removed when a retry succeeds or
when you dismiss them.
Permissions
The page is readable by anyone with access to the plugin. Retry and purge need update
(U) privilege; dismiss needs delete (D).
