npm.io
0.2.0 • Published 3d ago

@canner-ca/astro-cache

Licence
MIT
Version
0.2.0
Deps
0
Size
27 kB
Vulns
0
Weekly
0

@canner-ca/astro-cache

Canner caching for Astro. Three things in one zero-dependency package:

  1. A native Astro 7 cache providercacheCanner() plugs Canner into Astro's built-in route caching, the same API you'd use on Netlify/Vercel/Cloudflare.
  2. Draft mode — let editors preview unpublished content on the real URL without purging what everyone else sees.
  3. One-line cache headerscache(Astro.response, …) sets Cache-Control + Surrogate-Key correctly if you'd rather do it per route.

Works with any CMS (DatoCMS, Contentful, Sanity, Storyblak, …) — tags are just strings.

Install

npm install @canner-ca/astro-cache

Requires Node.js 20 or later. Zero runtime dependencies.

Option A — the cache provider (Astro 7+)

// 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 for invalidate()
    }),
  },
});
---
// src/pages/blog/[slug].astro — control caching per route
const post = await getPost(Astro.params.slug);
Astro.cache.set({ maxAge: 3600, swr: 86400, tags: [post.id, 'blog-listing'] });
---

Canner caches at its proxy (in front of your app, shared across instances), so the provider never spends your app's memory on an in-process cache — and invalidate() purges by tag or path through the same token.

Option B — one-line headers (any Astro version)

---
// src/pages/blog/[slug].astro
import { cache } from '@canner-ca/astro-cache';

const post = await getPost(Astro.params.slug);
// Cache 1h; keep serving up to a day past that while refreshing in the background.
cache(Astro.response, { ttl: 3600, swr: 86400, tags: [post.id, 'blog-listing'] });
---

That sets:

Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400
Surrogate-Key: <post.id> blog-listing

When the post changes, your CMS webhook purges tag <post.id> and Canner clears every page carrying it. Add blog-listing to your index and to the purge to clear it too.

Draft mode

Preview unpublished content on its real URL, without purging the published page:

// src/pages/api/draft.ts
import { enableDraft } from '@canner-ca/astro-cache';

export const GET = ({ url, cookies, redirect }) => {
  if (url.searchParams.get('secret') !== import.meta.env.PREVIEW_SECRET) {
    return new Response('Unauthorized', { status: 401 });
  }
  enableDraft(cookies, import.meta.env.CANNER_BYPASS_SECRET); // secret: Caching tab
  return redirect(url.searchParams.get('to') ?? '/');
};
---
// in your page, fetch draft content when the visitor is in draft mode
import { isDraft } from '@canner-ca/astro-cache';
const post = await getPost(Astro.params.slug, { draft: isDraft(Astro.request) });
---

enableDraft sets a hardened __canner_bypass cookie carrying your bypass secret; Canner then renders that visitor's requests fresh from origin while everyone else keeps getting the cached page. disableDraft(cookies) turns it back off. You can also bypass with a ?__canner_bypass=<secret> link or an X-Canner-Bypass: <secret> header.

Full guide (dashboard token, CMS webhook, bypass secret): https://canner.ca/docs/caching

API

cacheCanner(config)

Returns the object Astro's cache.provider expects. config: { slug?, token?, endpoint?, fetch? } (slug + token are required for invalidate()).

enableDraft(cookies, secret) / disableDraft(cookies) / isDraft(request)

Draft-mode helpers. cookies is Astro's cookies object; secret is your project's bypass secret (Caching tab). isDraft takes the Request and returns a boolean.

cache(target, options)

target is a Response, Astro.response, or a Headers. Returns the Headers.

applyCacheHeaders(headers, options)

Same thing, 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. Past ttl, Canner serves the stale copy instantly and refreshes once in the background. Sets stale-while-revalidate.
tags string | number | Array<string | number> Tags for purging. Sets Surrogate-Key. Numbers are coerced to strings; duplicates and whitespace-containing tags are dropped.
browserTtl number Optional. Seconds the visitor's browser may cache (sets max-age). Omit to let the browser revalidate while Canner serves from cache — keeps purges instant.

(The provider's Astro.cache.set() uses maxAge/swr/tags — Astro's own names.)

What it caches (and what it won't)

A response is cached by Canner only when all of these hold — the same rules this helper produces:

  • method GET/HEAD, status 200
  • Cache-Control: public with a positive s-maxage (or max-age)
  • no Set-Cookie
  • Vary absent or only Accept-Encoding
  • body under 8 MB

It only ever adds headers

This package never strips or mutates anything your app set. In particular it does not remove Set-Cookie: Canner already declines to cache a response that sets a cookie, so the only safe thing to do is tell you. If you call cache() on a response that sets a cookie, you'll get a development-only warning explaining it won't be cached — your cookie is left untouched.

Invalid input degrades gracefully too: a bad ttl sets no cache headers (the page still renders, just uncached) and warns in dev. 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).
  • The provider deliberately doesn't implement Astro's onRequest (in-process runtime caching): Canner caches out-of-process at its proxy, so a second in-app cache would just burn your app's memory duplicating it.

License

MIT

Keywords