A SvelteKit pattern for dynamic OpenGraph images (/og/<type>/... endpoint: SDK fetch -> satori -> PNG with Cache-Control: public, s-maxage=86400) hides a cache-poisoning footgun the moment the underlying data is per-user personalized.
The trap has two halves that are individually reasonable:
A module-level SDK fetch safety net. To make bare SDK calls work inside endpoint handlers,
hooks.server.tssetssdk_defaults.fetchto resolve the current request via kit'sgetRequestEvent()(AsyncLocalStorage) and route throughevent.fetch— and thehandleFetchhook injects the incomingcookieheader on every API-bound request. Every SDK call inside any+server.tstherefore silently carries the requester's auth cookies.A shared-cache header without
Vary: Cookie.s-maxage=86400on the PNG response means the CDN caches whatever it rendered first.
For viewer-independent cards (user profile, blog post) this is harmless. The moment one card type renders personalized data (e.g. an audience-cohort-segmented feed), a logged-in user hitting the OG URL primes the shared cache with their variant, and every crawler/user gets it for 24h.
Fixes, in preference order:
- Give the personalized card type an explicitly anonymous fetch: bypass
event.fetchentirely with a plainfetchthat reproduces only the URL rewrite (url.startsWith(PUBLIC_BASE) ? SSR_INTERNAL + url.slice(PUBLIC_BASE.length) : url) and never forwards cookies. You cannot de-cookieevent.fetch—handleFetchre-injects after your wrapper runs. - Shorten
s-maxageto below the content's refresh cadence and key the URL on the content period (/og/pulse/<edition-date>), so staleness windows are bounded even if the anonymity pin regresses.
General rule: audit every +server.ts that combines (a) SDK calls relying on module-default fetch and (b) shared-cache Cache-Control — the cookie forwarding is invisible at the callsite.