Edge caching CMS pages without leaking signed-in views
Turning on Cloudflare's edge cache for the marketing site took 10 lines of config. Making sure it never served one person's page to everyone took one more piece of middleware.

The public side of Emberstack (home page, pricing, blog, docs) is server-rendered from EmDash on every request. That's convenient for editing and wasteful for visitors, because the home page is the same for nearly everyone.
So I turned on edge caching. The config part was easy. The part worth writing about is the bug it would have caused.
The config
Astro's Cloudflare adapter ships a cache provider. Add it, then give the public routes a lifetime:
// astro.config.mjs
import { cacheCloudflare } from '@astrojs/cloudflare/cache'
export default defineConfig({
cache: { provider: cacheCloudflare() },
routeRules: {
'/': { maxAge: 300, swr: 86400 },
'/blog/[...path]': { maxAge: 300, swr: 86400 },
'/docs/[...path]': { maxAge: 300, swr: 86400 },
'/rss.xml': { maxAge: 900, swr: 3600 },
},
})maxAge: 300 keeps a page fresh for 5 minutes. swr: 86400 lets the edge keep serving the old copy for up to a day while it fetches a new one in the background, so a visitor never waits on a re-render.
Five minutes sounds long for a CMS. It mostly isn't, because EmDash tags every cached page with the content it used and purges those tags whenever someone saves. Edits show up straight away. The lifetime really only matters for scheduled posts going live on their own.
CMS pages at /<slug> (about, pricing, contact) set the same lifetime inside src/pages/[slug].astro instead. A root /[slug] rule would also match single-segment routes like /app and /_emdash, and I definitely don't want those cached.
After a deploy, the first request for a page reports cf-cache-status: MISS and every one after that is a HIT. /app keeps saying BYPASS, which is what I wanted.
The bug I almost shipped
Here's the problem. The edge cache keys on the URL. It doesn't know who's asking.
But Emberstack's public layout isn't the same for everyone. A signed-in app user sees a Dashboard link in the header. A CMS editor sees an Admin link and the EmDash editing toolbar.
So picture an editor opening /about right after a cache purge. Their request renders the page, the edge stores it, and for the next 5 minutes every anonymous visitor gets the editor's version of /about. With an Admin link. Possibly with a draft preview in it.
Nothing dangerous happens when someone clicks a link they can't use, but showing draft content to the public is a real leak.
The fix is one middleware
EmDash already handles its own case: when it injects the editor toolbar, it marks the response private, no-store and opts it out of the route cache. Nothing did the same for a signed-in *app* user, so I added a small handler that runs after the page renders:
export const privateCacheHandler: MiddlewareHandler = async (context, next) => {
const response = await next()
if (!context.locals.isLoggedIn && !context.locals.cmsUser) return response
if (!response.headers.get('content-type')?.includes('text/html')) return response
context.cache?.set(false)
response.headers.set('Cache-Control', 'private, no-store')
return response
}context.cache.set(false) tells the provider not to announce the response as cacheable, and the Cache-Control header stops the browser and anything in between from keeping it.
It has to run after next(), in middleware. I first tried it in the layout component, then realised that by the time the layout renders, the response headers may already be on their way out.
The trade-off I accepted
The flip side: a signed-in user who lands on a cached public page sometimes sees the signed-out header, with "Sign in" where Dashboard would be. One click into /app fixes it.
I'm fine with that. A stale "Sign in" link costs a second. Serving someone's private view to strangers costs trust.
If you add your own per-user bits to public pages, remember this post. Anything that varies by visitor either needs to come from the browser (a small fetch after load) or needs the page opted out of the cache.



