SR
Click to open github profile
Usage

Cache Invalidation

Learn how to clear the Nitro caches of your Nuxt app, whenever content changes in Craft CMS.


If you cache pages with route rules (swr, isr, cache) or use cached server handlers, Nuxt doesn't know when content changes in Craft. With the webhooks of the Query API, Craft notifies your Nuxt app, which then clears its caches.

Note

Requires Craft Query API 3.8.0 or later.

Craft Setup

Point the webhook to your Nuxt app and define a secret:

config/query-api.php
return [
  '*' => [
    'webhooks' => [
      [
        'url' => 'https://your-site.tld/api/craft/webhook',
        'secret' => '$QUERY_API_WEBHOOK_SECRET',
      ],
    ],
  ],
];

Nuxt Setup

Add the same secret to your runtime config. It's set via the NUXT_CRAFT_WEBHOOK_SECRET environment variable.

nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/**': { swr: 3600 },
  },
  runtimeConfig: {
    craftWebhookSecret: '',
  },
})

Then add a server route that verifies the signature and clears all Nitro caches:

server/api/craft/webhook.post.ts
import { createHmac, timingSafeEqual } from 'node:crypto'
import type { CacheWebhookPayload } from '@query-api/nuxt'

export default defineEventHandler(async (event) => {
  const { craftWebhookSecret } = useRuntimeConfig(event)
  if (!craftWebhookSecret) {
    throw createError({ statusCode: 500, statusMessage: 'Webhook secret is not configured' })
  }

  const body = (await readRawBody(event, 'utf8')) ?? ''
  const signature = getHeader(event, 'x-query-api-signature') ?? ''
  const expected = 'sha256=' + createHmac('sha256', craftWebhookSecret).update(body).digest('hex')
  if (
    signature.length !== expected.length ||
    !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  ) {
    throw createError({ statusCode: 401, statusMessage: 'Invalid signature' })
  }

  const payload = JSON.parse(body) as CacheWebhookPayload
  if (payload.event !== 'invalidate') {
    return { cleared: 0 }
  }

  // Nitro stores its caches under `cache:nitro:{routes,handlers,functions}`.
  const storage = useStorage('cache:nitro')
  const keys = await storage.getKeys()
  await Promise.all(keys.map((key) => storage.removeItem(key)))

  return { cleared: keys.length }
})

That's it. Whenever an editor saves an entry, Craft sends the webhook, the cache gets cleared and the next request renders the page with fresh content.

Warning

Don't use useStorage('cache:nitro').clear(). On a prefixed storage it skips entries that live in a parent mount, like the default in-memory storage, so nothing would be cleared.

Good to Know

Previews

Route rules can't bypass the cache for single requests. Craft's Live Preview reloads the same URL with the same token after every change, so a cached page would show outdated content. Don't cache routes that are used for previews, or serve previews from an instance without cache route rules.

Multiple Instances

The default Nitro storage lives in memory, so the webhook only clears the instance that receives it. If you run more than one instance, mount a shared driver (e.g. Redis) for the cache storage:

nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    storage: {
      cache: { driver: 'redis', url: process.env.REDIS_URL },
    },
  },
})

CDNs

Nitro sends Cache-Control headers for cached routes (s-maxage with swr, max-age without). A CDN in front of Nuxt, or without swr even the browser, caches the pages as well, and the webhook doesn't purge them. Prefer swr, and either disable HTML caching on the CDN or also purge the CDN in your webhook route.


Copyright © 2026 Samuel Reichör