Caching applies to server-rendered output — for Astro, the Node adapter (@astrojs/node); for Next.js and Nuxt, their standard server output. A fully static build renders at build time and doesn’t need per-request caching — redeploy to refresh it.
1. Turn caching on
In your project’s Settings → Caching, flip Response caching on. This puts Canner’s cache in front of your app. Nothing is cached until your responses ask for it — see step 2.
2. Mark responses cacheable & tag them
On the routes you want cached, set a Cache-Control header withs-maxage, and tag the response with one Surrogate-Key per entity on the page. Add stale-while-revalidate to keep serving instantly while a fresh copy is fetched in the background. Tags are scoped to your project automatically, so just use plain, readable names:
---
// src/pages/blog/[slug].astro (output: 'server')
const post = await getPost(Astro.params.slug);
// Cache on Canner for an hour; keep serving up to a day past that while a
// fresh copy is fetched in the background (stale-while-revalidate).
Astro.response.headers.set(
"Cache-Control",
"public, s-maxage=3600, stale-while-revalidate=86400"
);
Astro.response.headers.set(
"Surrogate-Key",
[`post-${post.id}`, "blog-listing"].join(" ")
);
---A single entity can appear on many routes (post page, listing, homepage). Tagging each response with the entity’s key lets one purge clear all of them at once. After a purge the next visitor to a cleared route triggers one fresh render, which is then cached again.
Prefer one line? Install @canner-ca/astro-cache and it sets both headers correctly — including the easy-to-forget public — and coerces numeric CMS ids for you:
---
import { cache } from '@canner-ca/astro-cache';
const post = await getPost(Astro.params.slug);
cache(Astro.response, { ttl: 3600, swr: 86400, tags: [post.id, 'blog-listing'] });
---Using Next.js? Server components can’t set response headers, so cache App Router pages from middleware.ts (or use Route Handlers /getServerSideProps). The @canner-ca/next-cache helper covers all three. Canner caches the HTML document and passes client-side RSC navigations straight through, so a cached page is never served stale to a soft navigation.
Using Nuxt? Use @canner-ca/nuxt-cache in a page (cache(useRequestEvent(), …)), a server route, or middleware. Nuxt fetches payloads from separate URLs, so pages cache cleanly; Canner passes any x-nuxt-no-ssr request through so the SPA shell is never cached.
Astro 7: use Canner as your cache provider
On Astro 7+, Canner plugs into Astro’s built-in route caching as a first-class provider — the same API you’d use on Netlify, Vercel, or Cloudflare, pointed at Canner. Configure it once:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import { cacheCanner } from '@canner-ca/astro-cache';
export default defineConfig({
adapter: node({ mode: 'standalone' }),
cache: {
provider: cacheCanner({
slug: 'my-project',
token: process.env.CANNER_CACHE_TOKEN, // only needed for invalidate()
}),
},
});Then control caching per route with Astro.cache.set() (orcontext.cache.set() in endpoints and middleware). Canner translates the directives into cache headers and handles tag- and path-based purges through the same token:
---
// src/pages/blog/[slug].astro
const post = await getPost(Astro.params.slug);
Astro.cache.set({ maxAge: 3600, swr: 86400, tags: [post.id, 'blog-listing'] });
---The provider caches at Canner’s proxy — in front of your app, shared across instances — so it never spends your app’s memory on an in-process cache. Purge by tag or by path from your CMS, or let the provider’s invalidate() do it.
Draft & preview: bypass the cache
To preview unpublished content on its real URL — without purging what everyone else sees — a request can bypass the cache. The public keeps getting the cached page; a request carrying your project’s bypass secret is rendered fresh from origin. It’s the same shape as Vercel’s Draft Mode. Three ways to carry the secret:
- a
__canner_bypasscookie (for a draft session — set it from a route you’ve authorized), - a
?__canner_bypass=<secret>query parameter (for a shareable preview link), - an
X-Canner-Bypass: <secret>request header (for programmatic use).
The framework helpers give you a turnkey draft route. Find your secret in Settings → Caching (or GET /projects/<slug>/cache/bypass-secret); rotate it there to invalidate every link at once:
// src/pages/api/draft.ts
import { enableDraft } from '@canner-ca/astro-cache';
export const GET = ({ url, cookies, redirect }) => {
// You gate this yourself — e.g. a secret in the CMS preview URL.
if (url.searchParams.get('secret') !== import.meta.env.PREVIEW_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
enableDraft(cookies, import.meta.env.CANNER_BYPASS_SECRET);
return redirect(url.searchParams.get('to') ?? '/');
};A bypassed response is never stored and never evicts the published entry, so draft content can’t leak into the public cache. In strict mode (the default) the secret must match; the opt-in cookie-name mode bypasses on the presence of the cookie alone, for apps that gate their own draft session.
3. Invalidate from your CMS
In Settings → Caching, generate a Purge token (shown once). In your CMS (any — DatoCMS’s native payload is understood automatically), add a webhook that fires on publish/unpublish and sends:
- URL:
https://api.canner.ca/projects/<your-slug>/cache/purge - Method:
POST - Header:
Authorization: Bearer <your purge token>
Send the tags your app emitted in Surrogate-Key, and/or exact paths to clear. Canner evicts every cached response carrying any of those tags — scoped to your project, so your tags can never affect another site even if the names collide:
curl -X POST https://api.canner.ca/projects/<your-slug>/cache/purge \
-H "Authorization: Bearer <your purge token>" \
-H "Content-Type: application/json" \
-d '{"tags": ["post-123", "blog-listing"]}'Invalidate by URL instead of (or alongside) tags with {"paths": [...]} — a bare path clears every query variant of it. This is what the Astro provider’s invalidate({ path }) calls under the hood:
curl -X POST https://api.canner.ca/projects/<your-slug>/cache/purge \
-H "Authorization: Bearer <your purge token>" \
-H "Content-Type: application/json" \
-d '{"paths": ["/blog/my-post"]}'Purge everything
Need a clean slate? The Purge everything button in Settings → Caching clears the whole project cache. Useful after a redeploy that changed layout or shared components — though deploys already purge automatically.
Static sites: rebuild instead of purge
If your site is fully static (no server adapter), there’s no per-request cache to purge — the pages were rendered at build time. For those, point your CMS webhook at the rebuild endpoint instead, using the same purge token. It redeploys your project from the connected GitHub repo:
curl -X POST https://api.canner.ca/projects/<your-slug>/cache/rebuild \ -H "Authorization: Bearer <your purge token>"
Rebuild needs a GitHub-connected project (it builds from your repo’s HEAD). SSR sites should use /cache/purge above — it’s instant and doesn’t cost a build.
Good to know
- Only responses with
Cache-Control: publicand a positives-maxageare cached. Authenticated orSet-Cookieresponses are never cached. - With
stale-while-revalidate, the first visitor after expiry gets the stale copy instantly while Canner refreshes it once in the background — no one waits on a cold render. - Purges are scoped to your project — you can never affect another tenant’s cache, and no one can purge yours without your token.
- Entries also expire on their own once
s-maxage(plus any SWR window) elapses; purging is just the fast path when content changes sooner. - A successful production deploy purges your cache automatically, so new code is live immediately.