# Caching & CDN setup

`Vary: Accept` is necessary but not always sufficient. Cloudflare, Fastly, and Vercel each have quirks you need to know before production.

Setting `Vary: Accept` is the baseline for content negotiation through
caches. Most CDNs honor it — but "honor" varies, and some behaviors
need explicit configuration. Here's what to check.

## Cloudflare

Cloudflare's default edge cache respects `Vary: Accept` on cacheable
responses. But a few gotchas:

- **Varying on more than a small set of values is discouraged.** If
  your `Accept` values are bounded (just `text/markdown` and `text/html`),
  you're fine. If you accept wild variety, Cloudflare may decline to
  cache.
- **Transform Rules can override.** If you've enabled HTML minification,
  compression tweaks, or email obfuscation, verify they don't strip
  `Vary`.
- **Tiered Cache and Smart Tiered Cache** honor `Vary` correctly, but
  cache-warming tools should be sent with the right `Accept` header or
  they'll populate the wrong representation.
- **Cache Rules** let you explicitly set the cache key. You can add
  `Accept` to the cache key manually:
  - *Cache Rules → Custom cache key → Include header: Accept*
- **Markdown for Agents** (Cloudflare's managed feature) handles all of
  this automatically. See
  [the guide](/guides/cloudflare-markdown-for-agents).

## Fastly

Fastly's edge cache honors `Vary: Accept` per HTTP spec. If your origin
returns the header, Fastly keys the cache on `Accept` automatically —
no custom VCL required. See
[Fastly's Vary documentation](https://www.fastly.com/documentation/reference/http/http-headers/Vary/).

Custom VCL is only needed if you want the edge to *add* a Vary header
the origin forgot, or to change the cache-key shape beyond what Vary
gives you. For append-at-edge:

```vcl
sub vcl_fetch {
  if (beresp.http.Vary) {
    set beresp.http.Vary = beresp.http.Vary ", Accept";
  } else {
    set beresp.http.Vary = "Accept";
  }
}
```

## Vercel

Vercel's edge cache respects `Vary: Accept` on `Cache-Control: s-maxage=...`
responses. For Next.js App Router:

- `export const revalidate = N` in a route — respects Vary.
- Route handlers that return `Cache-Control: s-maxage=60` + `Vary: Accept` —
  cached per-variant at the edge.
- ISR (`getStaticProps` + `revalidate`) — Vary works, but you need to
  trigger regeneration for both variants.

Vercel's dashboard shows cache hits per variant; check that both
Markdown and HTML requests get `x-vercel-cache: HIT` after warmup.

## Netlify

Netlify Edge respects `Vary: Accept` via the standard Cache-Control
semantics. The Netlify-specific quirk: if you're using Netlify Functions
or Edge Functions, ensure the function itself returns the `Vary` header —
don't rely on `_headers` file rules for dynamic responses.

## Origin-level caching

If your own reverse proxy is doing the caching (not a CDN in front of
it), add Accept to the cache key. For Nginx:

```nginx
proxy_cache_key "$scheme$request_method$host$request_uri$http_accept";
```

## Testing your cache

```bash
# Prime the cache with Markdown
curl -sI -H "Accept: text/markdown" https://yoursite.com/article

# Then with HTML — should get a distinct response (new cache entry)
curl -sI -H "Accept: text/html" https://yoursite.com/article

# Then request Markdown again — should be a cache HIT now
curl -sI -H "Accept: text/markdown" https://yoursite.com/article
```

Check for `age:`, `x-cache:`, or `cf-cache-status:` headers to confirm
which request was served from cache. If all three come from origin,
your cache isn't using Vary correctly.

## The "forgot Vary entirely" failure mode

No `Vary: Accept` on a negotiated response is the single most common
production bug. Symptoms:

- Agents report they're getting HTML with `Content-Type: text/html`,
  but `curl` shows the Markdown is being served correctly
- Users in a browser see raw Markdown on a page that should be HTML
- Cache hit rates look normal; the cache is just serving the first-seen
  representation to everyone

Fix: add `Vary: Accept` to every negotiated response, and purge your
cache. Until you purge, cached entries are still keyed on URL alone and
will keep serving the wrong thing.