---
title: Caching
description: Keep the pages of your site at the edge, and purge exactly what changed when you publish, edit or unpublish, in seconds.
section: guides
order: 7
---

# Caching

A page that reads Hollware does not have to be rendered for every reader. Hollware knows exactly what changed when you publish, edit or unpublish an entry, so your site keeps its pages at the edge and purges only those that showed it. Most requests then run nothing at all, and a change shows in seconds everywhere.

## With Nuxt

Turn it on in `nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  modules: ['@hollware/nuxt'],
  hollware: { site: 'my-site', cache: 'purge' },
})
```

and give the site's Worker its cache in `wrangler.jsonc`:

```jsonc
{
  "cache": { "enabled": true },
}
```

Each page is then kept by Cloudflare's Workers Cache, which serves it without running your Worker, tagged with what it showed. Errors, writes and pages that read nothing are never kept, and the browser keeps nothing: only the edge does.

Then tell Hollware where to send its events. In the admin, under **Site**, add a webhook named `site` with the URL `https://your-site.example/_hollware/webhook`. Hollware makes its secret and shows it once: set it on the site's Worker.

```sh
npx wrangler secret put NUXT_HOLLWARE_WEBHOOK_SECRET
```

From then on, every publish, edit and unpublish purges the pages that showed that entry, its lists and the pages that link to its translations, and a change of the site's structure (a collection, a route, a locale) purges the pages that show the structure and every list. On hollware.com, a published page shows about eight seconds after it is approved.

::callout{type="tip"}
`cache: 30` instead keeps each page 30 seconds in every isolate, with no webhook: simpler, and a change shows within that time.
::

## The contract, for any other stack

Every event Hollware sends carries `cacheTags`, the tags the change invalidates:

```json
{
  "kind": "entry.published",
  "entry": { "id": "ent_01JAB3XQ8ZK4M6N2P5R7S9T0VW", "collection": "blog", "locale": "en", "slug": "hello-world" },
  "path": "/blog/hello-world/",
  "cacheTags": ["entry:ent_01JAB3XQ8ZK4M6N2P5R7S9T0VW", "list:blog:en", "list:blog:*", "group:blog:hello-world"]
}
```

A change of the site's structure sends `site.changed`, which carries no entry, only the tags: `site`, and the lists of every collection in every locale the site had or has.

Tag each page with what it shows, in the same names, and purge the tags of each event:

| Tag | A page that |
| --- | --- |
| `entry:<id>` | shows the entry, alone or among others |
| `list:<collection>:<locale>` | lists the entries of a collection in a locale |
| `list:<collection>:*` | lists them in every locale |
| `group:<collection>:<group>` | links to the translations of a group |
| `site` | shows the structure of the site, such as its navigation |

`@hollware/core` exports the functions that make them: `entryTag`, `listTag`, `groupTag`, `tagsOfChange` and `tagsOfSiteChange`. Check each event's signature with `verifyWebhook` before you purge anything.

```ts
import { parseHollwareEvent, verifyWebhook } from '@hollware/core'

const body = await request.text()
const ok = await verifyWebhook({
  secret: env.HOLLWARE_WEBHOOK_SECRET,
  body,
  headers: request.headers,
  // Refuses an event it was given before, by the id in the signed body: a captured request
  // sent again within the five minutes of tolerance is refused too. Keep each id for a day.
  isNew: async (id) => {
    if ((await env.SEEN.get(id)) !== null) return false
    await env.SEEN.put(id, '1', { expirationTtl: 86_400 })
    return true
  },
})
if (!ok) return new Response(null, { status: 401 })
const event = parseHollwareEvent(JSON.parse(body))
```

`verifyWebhook` alone, without the rest of `@hollware/core`, is in `@hollware/client/webhooks`, which imports nothing: a site on another major version of zod checks its deliveries with it.

A retry of a delivery carries the same id: if your receiver can fail after `isNew` has recorded an id, record it only once the event is handled, so the retry is not refused.
