npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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).