Published content is served from caches and purged by tag when something changes; nothing is rebuilt unless your site is a static build. Two layers cache content for a custom-code site: the platform caches delivery API answers, and your site caches the SDK's answers. A publish empties both, the platform's directly and yours through a signed webhook. This page describes both layers, what each change purges, and how to fit your own caching in.
The two layers
| Layer | What | Key and tags | Lifetime | Purged by |
|---|---|---|---|---|
| Platform: delivery API | every read answer of /api/v1/*, per site and query | tag cms:site:<siteId> (brand answers also brand:<workspaceId>) | at most 1 hour | publish, trash, restore, scheduled moments, schema push, seed, Cache legen |
| Your site, Next.js | every SDK GET in Next's data cache | kwisple:all + a per-method tag | revalidate, default 3600 s | the webhook → createRevalidateRoute |
| Your site, Astro (on demand) | delivery answers in the server's memory | the request URL | cacheTtl, default 3600 s; at most 2000 answers | the webhook → the injected /api/revalidate |
| Your site, static build | the built HTML | none | until the next build | a rebuild (deploy hook or Opnieuw bouwen) |
Reads made with the development model (kwisple dev, x-kwisple-schema: dev) are never cached
on the platform. Preview reads (x-kwisple-preview) are never cached anywhere.
HTTP headers of the delivery API
Delivery answers are bearer-authenticated, so they are never marked cacheable for shared
caches. Cached reads answer cache-control: private, max-age=60, stale-while-revalidate=600
with vary: authorization, x-kwisple-preview; preview reads and writes answer
private, no-store. A CDN in front of the platform never serves one token's answer to another
caller.
Cache tags
Platform tags (cms:*)
| Tag | Covers |
|---|---|
cms:site:<siteId> | every delivery read of the site |
cms:section:<siteId>:<handle> | reads that list a section |
cms:entry:<entryId> | one entry |
cms:uri:<siteId>:<locale>:<uri> | the "which entry lives at this URI" lookup |
cms:globals:<siteId> | global sets |
cms:redirects:<siteId> | redirects |
cms:schema:<siteId> | the content model |
These tags are also sent to your site in the webhook, so you can tag your own cached work with them (see Patterns for Next.js).
SDK tags (kwisple:*)
Every SDK GET that is not a preview read carries kwisple:all (SITE_TAG, exported from
@kwisple/sdk). Some methods add a second tag:
| Method | Extra tag | Notes |
|---|---|---|
site() | kwisple:site | also memoised per client instance |
brand() | kwisple:brand | |
privacy() | kwisple:privacy | |
matchRedirect() | kwisple:redirects | cached for 300 s, misses included |
remoteSchema(), exportSeed(), formSlots(), stagingInfo(), verifyPreview() | none | never cached |
The revalidate routes always purge kwisple:all, so every SDK answer goes on every webhook,
whatever tags the webhook carries. The extra tags are for your own revalidateTag calls.
What publishing purges
When an editor clicks Zet online (publish), the dashboard runs afterContentChange
(apps/dashboard/src/lib/services/cms-publish.ts) after the content is saved:
- Tags.
publishEntry(in@kwisple/content) returns the entry's tag, its section's tag and the tags of every entry that relates to it (from the relation index), so a listing or a page that shows this entry is purged too.cms:site:<siteId>is always added. - Platform purge. For a custom-code site every tag expires hard, before the webhook goes out, so your site's refetch gets fresh answers. For hosted sites the broad tags are refreshed in the background and only the edited entry's tags expire hard; its public paths are re-warmed.
- Webhook. A custom-code site with a webhook URL gets the signed webhook with the same tags
and
event: "cms.published". - Consent services. Newly embedded videos, maps or pixels are added to the site's consent configuration in the background.
- Rebuild. For a site whose hosting is set to Opnieuw bouwen, a debounced rebuild is queued (at most one per minute).
- Scheduled purges. A future Online vanaf or Offline vanaf gets a purge at that moment (see Scheduled moments).
A failure in any step never fails the publish: the editor sees De site kon niet verwittigd
worden: <reason>. De inhoud is wel opgeslagen. and the content is live on the platform.
Other changes and their events
| Change | event in the webhook | Tags |
|---|---|---|
| Publish an entry | cms.published | entry, section, related entries, cms:site |
| Move an entry in a structure | cms.moved | entry, section, cms:site |
| Move to trash / restore | cms.trashed / cms.restored | entry, section, related entries, cms:site |
| Save a global set | cms.global.saved | globals, cms:site |
| Save or delete a redirect | cms.redirect.saved / cms.redirect.deleted | redirects, cms:site |
| An Online vanaf / Offline vanaf moment passes | cms.scheduled | the entry's tags |
| Cache legen | cache.purge | kwisple:all, every cms:* site tag, every entry tag (at most 200) |
| Brand kit saved | brand.updated | kwisple:brand |
| Privacy or consent settings saved | privacy.updated | kwisple:privacy, kwisple:all |
The brand webhook goes to every custom-code site of the client, staging included. A
kwisple push purges the platform's cache for the site but sends no webhook: the deployment
that follows the push in CI, or Cache legen, refreshes your site.
The revalidation webhook
Set the URL under Website → Instellingen → Ontwikkeling in Webhook bij publiceren
(webhook on publish), normally https://<your site>/api/revalidate. Saving it creates the
Webhook-geheim (webhook secret) when the site has none; put that in
KWISPLE_WEBHOOK_SECRET. kwisple link writes it to .env for local work.
Request
POST /api/revalidate
content-type: application/json
x-kwisple-signature: 5d1c…e9 (hex)
{
"tags": ["cms:entry:ent_…", "cms:section:site_…:news", "cms:site:site_…"],
"event": "cms.published",
"siteId": "site_…",
"at": "2026-10-30T09:12:44.120Z"
}| Part | Meaning |
|---|---|
x-kwisple-signature | HMAC-SHA256 of the raw body with the site's webhook secret, hex encoded |
tags | cache tags that changed; the SDK routes read at most 200 |
event | what happened (table above) |
siteId | the site the webhook is for |
at | when it was sent, ISO 8601 |
Delivery rules
- One attempt with a 5 s timeout, no retries. The result goes back to the editor.
- Redirects are never followed: a URL that redirects fails with
Webhook-URL stuurt door; gebruik de eindbestemming. - In production the URL must be https and resolve to a public address. During local development
http and
localhostwork. - Any 2xx answer counts as delivered.
The SDK routes
Next.js, one file:
// src/app/api/revalidate/route.ts
import { createRevalidateRoute } from "@kwisple/sdk/next";
export const POST = createRevalidateRoute({ secret: process.env.KWISPLE_WEBHOOK_SECRET });It verifies the signature, then calls revalidateTag(tag, { expire: 0 }) for kwisple:all and
every tag in the body, and answers { revalidated: true, tags }.
Astro: the kwisple() integration injects /api/revalidate when the site renders on demand. It
verifies the signature and empties the whole memory cache (clearKwispleCache()), answering
{ revalidated: true, cleared, tags }.
Both answer 401 with Ongeldige handtekening. when the signature does not match and 400
with Body is geen JSON. when the body is not JSON. To verify by hand in another framework, use
verifySignature(secret, rawBody, signature) from @kwisple/sdk/next; compute it over the raw
body, before parsing.
Cache legen
Cache legen (clear cache) under Website → Instellingen → Algemeen is for agency staff
(the site.advanced right). It asks Zeker? Cache legen before it runs, then:
- expires every platform tag of the site at once: delivery answers, host lookups, privacy, brand, every section, entry and URI (the agency's shared footer-credit tag is only marked stale);
- sends the webhook with
event: "cache.purge"to a custom-code site; - reports Cache geleegd: with platform and website marked gelukt or mislukt,
or
website (webhook mislukt)with the reason. Without a webhook URL the reason isGeen webhook ingesteld.
Use it after an infrastructure change (for example a new storage address that cached pages still point at) or when a site shows something old and you want to rule caching out.
The platform-wide purge
Platform administrators have Alles legen (empty everything) in Platformbeheer. It queues the
cache.purgeAll job, which runs the same purge and webhook for every live site of every live
client, one after the other, and logs the counts. A second click within 30 seconds gets
Er loopt al een opdracht om alles te legen.
Scheduled moments
A published page is cached until something purges it, so it would not appear or disappear on
its own at Online vanaf or Offline vanaf. When an entry is published with a future
moment, the dashboard queues a cms.scheduled job for that moment plus five seconds (so the date
has really passed when your site refetches). The job purges the entry's tags on the platform and
sends the webhook with event: "cms.scheduled", exactly like a publish. Jobs are deduplicated per
entry and moment; a moment that is changed later simply purges once more. A site that went to the
trash in the meantime is skipped.
Patterns for Next.js
Every SDK read is already cached and purged; most sites need nothing more. The starter's catch-all route renders on first request and is then served from the cache:
// src/app/[[...path]]/page.tsx
export const dynamicParams = true;
export function generateStaticParams(): { path?: string[] }[] {
return [];
}Keep request APIs (headers(), cookies()) out of cached routes, or every page renders per
request again. Draft mode (live preview) bypasses the cache by itself.
When you cache your own work (a computed menu, a third-party call that combines with content), tag it so the webhook reaches it:
import { SITE_TAG } from "@kwisple/sdk";
import { unstable_cache } from "next/cache";
import { client } from "@/lib/cms";
export const getMenu = unstable_cache(
async () => {
const tree = await client.tree("pages", { maxLevel: 2 });
return tree.map((n) => ({ label: n.title, href: n.url, children: n.children.length }));
},
["menu"],
{ tags: [SITE_TAG] },
);For finer control, tag with the platform tags from the webhook (cms:entry:<id>,
cms:section:<siteId>:<handle>); createRevalidateRoute purges every tag it receives.
The client's revalidate option sets the time-based fallback (default 3600 s). Lower it only if
your site cannot receive the webhook.
Patterns for Astro
- On demand (recommended).
output: "server"with an adapter. The integration wraps the client'sfetchin the memory cache (GET answers with status 200 or 404, no preview reads), injects/api/revalidate, and turns the cache off inastro dev. Tune it withkwisple({ cacheTtl: 600 }). CallclearKwispleCache()yourself if you need to. - One process. The memory cache lives in one server process. When you run several instances
behind a load balancer, the webhook empties only the instance that receives it; the others
serve old answers until
cacheTtlpasses. Run one instance or lowercacheTtl. - Static. A static build has no route to purge. Under Hosting & deploys set Als inhoud verandert to Opnieuw bouwen (rebuild on every publish, at most once a minute), or point Webhook bij publiceren at your host's deploy hook.
Proxies and CDNs in front of your site
The webhook purges caches the platform and the SDK control. It cannot purge a CDN or proxy that
caches your HTML. The Cloudflare proxy (orange cloud) caches HTML for a while and keeps serving
the old page after a publish, so content no longer appears within seconds. Either set the site's
DNS records to DNS only (grey cloud), or configure the CDN not to keep HTML longer than about
a minute. Caching static files (/_next/static, fonts, images) is fine.
Preview pages must never be cached by a CDN: they answer private, no-store (the Astro
middleware sets this for previewed responses).
Checklist
/api/revalidateis deployed and Webhook bij publiceren points at it (final URL, https).KWISPLE_WEBHOOK_SECRETequals Webhook-geheim; after Vernieuwen (renew), redeploy.- Your own caches carry
kwisple:allor a platform tag. - Static sites rebuild on publish.
- No CDN keeps your HTML for long.
- After a publish, the dashboard shows no De site kon niet verwittigd worden message.