Kwisple
Browse the docs

Your site repository › Rendering

Live preview and revalidation

How the dashboard previews drafts on your site, how a publish reaches your cache through the signed webhook, what "Cache legen" does, and when a site rebuilds instead.

Two flows connect the dashboard to your site. Live preview shows an editor's unsaved draft inside the entry editor, rendered by your own code. Revalidation tells your site that published content changed, so cached pages are dropped and the next visitor gets the new version. Both are a route in your site plus a setting in the dashboard, and both are already wired up in the starters.

Live previewRevalidation
Dashboard settingPreview-URLWebhook bij publiceren (webhook on publish) + Webhook-geheim (webhook secret)
WhereWebsite → Instellingen → Ontwikkeling (development), card SiteSame card
Site routeGET /api/preview?token=…POST /api/revalidate
Next.jscreatePreviewRoute(client) + <LivePreview />createRevalidateRoute({ secret })
Astroinjected by kwisple() + <LivePreview />injected by kwisple()
Token scopepreviewnone (HMAC signature with the webhook secret)

kwisple link fills in the Preview-URL with http://localhost:<dev port>/api/preview when it is still empty and generates the webhook secret. For production you set both to your live address (Quick start).

Live preview

The flow

  1. An editor opens the preview in the entry editor. The dashboard creates a signed preview token (valid for one hour, bound to the site, the entry, the locale and the person) and opens your Preview-URL with ?token=… in an iframe. Without a Preview-URL the editor sees "Stel eerst een preview-URL in bij Instellingen › Ontwikkeling."
  2. Your /api/preview route calls client.verifyPreview(token), which asks GET /api/v1/preview/verify (the site token needs the preview scope). The platform answers with the entry id, the locale and where to send the editor: the entry's public path, or /__preview?entry=<id>&locale=<locale> for an entry that was never published and has no URI yet.
  3. The route stores the token in an HTTP-only cookie site_preview (one hour; SameSite=None; Secure in production because the dashboard frames the site cross-site, Lax locally) and answers 307 to that path. In Next.js it also turns on draft mode.
  4. The page renders with the per-request client (getClient), which switches to client.withPreview(token). Single-entry reads (entry(), and entryByPath() which uses it) then send x-kwisple-preview and return the draft; they are never cached. Listings, trees and globals keep returning published content.
  5. <LivePreview /> in the layout posts { type: "kwisple:preview:ready" } to the dashboard. After every autosave the dashboard posts { type: "kwisple:preview:refresh" } back, and the page re-renders in place with the scroll position kept. A site that never says it is ready gets a full iframe reload instead.

The route answers 400 "Preview-token ontbreekt." without a token and 401 "Preview-token ongeldig of verlopen. Open de preview opnieuw vanuit het dashboard." when the platform rejects it. ?exit=1 leaves preview mode (and deletes the cookie); add &redirect=/some/path to land somewhere other than /. Only paths on your own site are accepted there.

Next.js

// src/app/api/preview/route.ts
import { createPreviewRoute } from "@kwisple/sdk/next";
import { client } from "@/lib/cms";
export const GET = createPreviewRoute(client);
// src/app/layout.tsx (excerpt)
import { LivePreview } from "@kwisple/sdk/next";
import { draftMode } from "next/headers";

const draft = await draftMode();
// …inside <body>:
{draft.isEnabled ? <LivePreview allowedOrigin={process.env.KWISPLE_DASHBOARD_ORIGIN} /> : null}

<LivePreview> is a client component: it calls router.refresh() on a refresh message, which re-runs your server components with the draft, and shows a small "Voorbeeldweergave" badge in the corner. Messages from any origin other than allowedOrigin are ignored. getPreviewToken() returns the token in draft mode (else null); PREVIEW_COOKIE is the cookie name.

Draft mode bypasses Next's full-route cache, so previewed pages render per request while every other visitor keeps the cached page. Read draftMode() in the layout, never headers() or cookies().

Astro

The kwisple() integration injects /api/preview when the site renders on demand (output: "server" or an adapter). Render the component while previewing:

---
import { LivePreview } from "@kwisple/sdk/astro/components";
import { kwispleEnv, previewToken } from "@kwisple/sdk/astro/server";
const previewing = Boolean(previewToken(Astro));
---
{previewing ? <LivePreview allowedOrigin={kwispleEnv.dashboardOrigin} /> : null}

On a refresh message the Astro component fetches the current page again and swaps the <body> in place (falling back to a full reload on an error). The integration's middleware sets Cache-Control: private, no-store and X-Robots-Tag: noindex on every response while the preview cookie is present, so drafts never reach a CDN or a search engine.

Entries without a URL: /__preview

A never-published entry has no URI, so the platform sends the editor to /__preview?entry=<id>&locale=<locale>. The Next.js starter answers it in src/app/%5F%5Fpreview/page.tsx (the folder name is URL-encoded because Next treats folders starting with _ as private): it reads api.entry({ id, locale }) with the per-request client, calls notFound() outside draft mode, and is marked robots: { index: false, follow: false }. robots.txt disallows /__preview.

The Astro starter has no /__preview page: such entries reach the catch-all and render the 404 until they have been published once.

Previewing a branch

When the repository is connected under Hosting & deploys, the preview has a Voorbeeld op (preview on) menu for the agency team. It lists ready preview deployments, the newest per branch. Choosing one opens the same preview path on that deployment (<deployment URL><path of your Preview-URL>?token=…, default path /api/preview). The preview deployment needs the KWISPLE_* variables of the site, including a token with the preview scope. See Hosting and deploys.

Framing

The dashboard shows your site in an iframe. Neither starter sends X-Frame-Options or a frame-ancestors policy, so framing works out of the box. If you add security headers, allow the dashboard: Content-Security-Policy: frame-ancestors 'self' <KWISPLE_DASHBOARD_ORIGIN> (and the agency's own dashboard host when it works white-label). Never send X-Frame-Options: DENY. Details in SEO, performance and security headers.

Revalidation

The webhook

After every content change the platform first drops its own caches for the site, then sends one POST to the Webhook bij publiceren. The help text in the dashboard gives the shape: https://acme.be/api/revalidate. The Webhook-geheim is created as soon as a webhook URL is saved; Vernieuwen (renew) makes a new one, which you then also set as KWISPLE_WEBHOOK_SECRET in the site.

POST /api/revalidate HTTP/1.1
content-type: application/json
x-kwisple-signature: 5f2c…e91a

{"tags":["cms:entry:…","cms:site:…"],"event":"cms.published","siteId":"…","at":"2026-10-30T09:12:44.120Z"}
FieldMeaning
tagsCache tags that changed (at most 200 are read).
eventWhat happened (table below).
siteIdThe site.
atWhen the platform sent it (ISO 8601).

The signature is the HMAC-SHA256, in hex, of the raw body with the webhook secret. verifySignature(secret, body, signature) (exported from @kwisple/sdk/next) compares it in constant time; verify the body exactly as received, before parsing it.

EventSent when
cms.publishedAn entry is published.
cms.movedAn entry moves in a structure.
cms.trashed, cms.restoredAn entry goes to the trash or comes back.
cms.scheduledA page's "online vanaf" or "offline vanaf" moment arrives.
cms.global.savedA global set is saved.
cms.redirect.saved, cms.redirect.deletedA redirect changes.
cms.schema.pushedA content model is pushed.
brand.updatedThe brand kit changes (tag kwisple:brand).
privacy.updatedCookie and privacy settings change.
cache.purgeSomeone clicked Cache legen (below).

The platform waits five seconds for an answer, never follows redirects (a 3xx is reported as "Webhook-URL stuurt door; gebruik de eindbestemming.") and in production only calls https addresses on public hosts. A failed webhook never blocks publishing: the editor sees why the site did not answer.

Tags

TagWhere it comes from
kwisple:allOn every SDK read. Both SDK routes always purge it.
kwisple:site, kwisple:brand, kwisple:privacy, kwisple:redirectsAdded by the SDK to site(), brand(), privacy() and matchRedirect().
cms:site:<siteId>In every payload.
cms:entry:<entryId>, cms:section:<siteId>:<handle>, cms:globals:<siteId>, cms:redirects:<siteId>, cms:schema:<siteId>Payload tags for what changed.

Because kwisple:all is always purged, you only need the cms:* tags for your own unstable_cache or fetch calls that do not go through the SDK.

Next.js

// src/app/api/revalidate/route.ts
import { createRevalidateRoute } from "@kwisple/sdk/next";
export const POST = createRevalidateRoute({ secret: process.env.KWISPLE_WEBHOOK_SECRET });

The route verifies the signature (401 unauthorized, "Ongeldige handtekening."), parses the tags (400 bad_request, "Body is geen JSON."), calls revalidateTag(tag, { expire: 0 }) for kwisple:all and every payload tag, and answers { "revalidated": true, "tags": [...] }. Expiring hard matters: the next request refetches from the platform at once instead of being served the old page once more.

Astro

The injected /api/revalidate verifies the signature the same way and empties the whole in-memory content cache (clearKwispleCache(), also exported from @kwisple/sdk/astro/server). It answers { "revalidated": true, "cleared": <entries dropped>, "tags": [...] } with Cache-Control: no-store. The cache lives per server process; with several processes each needs its own webhook call, or a shorter cacheTtl.

Time-based fallback

Without a webhook, cached answers still expire: the Next data cache after revalidate seconds (default 3600), the Astro memory cache after cacheTtl seconds (default 3600), redirect lookups after 300 seconds. Keep the webhook; the timers are a safety net.

Testing the route locally

body='{"tags":[],"event":"test"}'
sig=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$KWISPLE_WEBHOOK_SECRET" | sed 's/^.* //')
curl -i -X POST http://localhost:4102/api/revalidate \
  -H "content-type: application/json" -H "x-kwisple-signature: $sig" --data "$body"

Use port 4104 for the Astro starter. A wrong secret gives the 401.

"Cache legen"

Website → Instellingen → Algemeen → Cache legen (empty cache) is for the agency team. After the confirmation Zeker? Cache legen it empties every cache the platform holds for the site and then sends the webhook with event: "cache.purge" and kwisple:all plus the site's cms:* tags, so your site empties its own cache too. The result line Cache geleegd: shows platform and website, each with a tick or a cross; when your route did not answer with a 2xx it reads website (webhook mislukt) followed by the reason (for example "Site antwoordde met 401." or "Geen webhook ingesteld."). Use it after an infrastructure change, such as a new storage address that cached pages still point at. Publishing never needs it.

Static builds versus server-rendered sites

Revalidation needs a running server that can drop a cache. A fully static build has none, so content changes need a new build.

SiteOn publishHow to set it up
Next.js with ISR (the starter), Astro with output: "server" (the starter)The webhook purges the cache; the next request renders fresh content. No rebuild.Webhook bij publiceren → https://<site>/api/revalidate. With a connected repository: Als inhoud verandert → Alleen verversen (only refresh).
Static build (Astro output: "static", a static export)A new build of the site.Connect the repository and choose Opnieuw bouwen (rebuild): every publish queues one build, at most one per minute per site. Without a connected repository, point Webhook bij publiceren at your host's deploy hook.

A static Astro build has no /api/preview or /api/revalidate (the integration logs this at build time), so live preview needs an on-demand deployment, for example a preview deployment. How the rebuild is scheduled is in Hosting and deploys; the caches on the platform side in Caching.

Troubleshooting

SymptomCause and fix
The preview stays empty or shows your loginThe Preview-URL does not point at a running /api/preview, or the site's token lacks the preview scope.
401 "Preview-token ongeldig of verlopen"The token is older than an hour or belongs to another site: open the preview again from the dashboard.
The preview does not update while typing<LivePreview> is not rendered (Next: only while draftMode() is on), or allowedOrigin differs from the dashboard's origin.
The iframe is blank in productionA frame-ancestors or X-Frame-Options header blocks the dashboard.
Published changes do not showCheck the publish message in the editor: a 401 means KWISPLE_WEBHOOK_SECRET differs from Webhook-geheim; a 404 means the route is missing at that address.
"Webhook-URL stuurt door"The URL redirects (http to https, bare domain to www): enter the final address.

Last updated 2026-10-30