@canner-ca/next-cache
One-line cache headers for Canner, for Next.js. It sets
Cache-Control and Surrogate-Key exactly the way Canner's cache proxy expects,
so your pages cache on Canner and purge by tag from your CMS.
Install
npm install @canner-ca/next-cache
Requires Node.js 20 or later. Zero runtime dependencies.
Use it
Call cache() wherever you set response headers in Next.js.
Route Handlers (app/.../route.ts):
import { NextResponse } from 'next/server';
import { cache } from '@canner-ca/next-cache';
export async function GET() {
const posts = await getPosts();
const res = NextResponse.json(posts);
cache(res, { ttl: 3600, tags: ['blog-listing'] });
return res;
}
Middleware (middleware.ts) — this is how you cache App Router pages,
since server components can't set response headers themselves:
import { NextResponse } from 'next/server';
import { cache } from '@canner-ca/next-cache';
export function middleware(request) {
const res = NextResponse.next();
if (request.nextUrl.pathname.startsWith('/blog/')) {
// Tag by a path-derived key your CMS webhook can purge.
const slug = request.nextUrl.pathname.split('/')[2];
cache(res, { ttl: 3600, tags: [slug, 'blog-listing'] });
}
return res;
}
Pages Router (getServerSideProps / API routes) — pass the Node response:
export async function getServerSideProps({ res, params }) {
const post = await getPost(params.slug);
cache(res, { ttl: 600, tags: [post.id] });
return { props: { post } };
}
Full guide (the webhook setup, the dashboard token): https://canner.ca/docs/caching
How App Router caching works
A Next.js App Router URL serves two things: the HTML document (a cold load,
a crawler, a hard navigation — no RSC header) and an RSC payload (client
navigation/prefetch — sends an RSC header). Canner caches only the HTML
document and passes every RSC request straight through to your app. So marking
a page cacheable is safe: client-side navigation is never served a stale or
mismatched response, and the cache speeds up exactly the requests that matter
for SEO and first paint.
You don't configure any of that — it's how Canner's proxy treats Next.js. You just set the headers (via middleware for pages, or directly in route handlers).
API
cache(target, options)
target is a Response/NextResponse, a Node ServerResponse/NextApiResponse,
or a Headers. Returns the same target for chaining.
applyCacheHeaders(headers, options)
Lower-level, when you already hold a Headers instance.
options
| Option | Type | Notes |
|---|---|---|
ttl |
number (required) |
Seconds Canner may serve the cached response. Sets s-maxage. Positive integer. |
swr |
number |
Optional. Stale-while-revalidate window in seconds — Canner serves the stale copy instantly past ttl while refreshing once in the background. Sets stale-while-revalidate. |
tags |
string | number | Array<string | number> |
Tags for purging. Sets Surrogate-Key. Numbers are coerced; duplicates and whitespace tags are dropped. |
browserTtl |
number |
Optional. Seconds the visitor's browser may cache (sets max-age). Omit to keep tag purges instant. |
Draft mode
Preview unpublished content on the real URL without purging the published page. Set
the bypass cookie from a route you've authorized — it pairs with (doesn't replace)
Next's own draftMode():
// app/api/draft/route.ts
import { cookies } from 'next/headers';
import { enableDraft } from '@canner-ca/next-cache';
export async function GET(request: Request) {
const secret = new URL(request.url).searchParams.get('secret');
if (secret !== process.env.PREVIEW_SECRET) return new Response('no', { status: 401 });
enableDraft(await cookies(), process.env.CANNER_BYPASS_SECRET!); // secret: Caching tab
return Response.redirect(new URL('/', request.url));
}
enableDraft accepts the cookies() store, a NextResponse, or a Node
ServerResponse (Pages Router). disableDraft(target) turns it off; isDraft(request)
tells you whether to fetch draft content. You can also bypass with a
?__canner_bypass=<secret> link or an X-Canner-Bypass: <secret> header.
What Canner caches
The same rules this helper produces:
- method
GET/HEAD, status200, noRSCrequest header Cache-Control: publicwith a positives-maxage(ormax-age)- no
Set-Cookie Varyabsent,Accept-Encoding, or the Next.js router tokens (which Canner handles via the HTML/RSC split above)- body under 8 MB
It only ever adds headers
This package never strips or mutates anything your app set. It does not
remove Set-Cookie: Canner already declines to cache a response that sets a
cookie, so you get a development-only warning instead — your cookie is left
untouched. A bad ttl sets no headers and warns; only passing something that
isn't a response/Headers throws.
Non-goals
- It doesn't configure your CMS webhook — that's two copy-paste values in the Canner dashboard (Settings → Caching).
For Astro (including a native cache provider), see
@canner-ca/astro-cache.
License
MIT