Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion public/.well-known/agent-skills/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "skill-md",
"description": "Query and apply The Website Specification — a platform-agnostic specification of what a good website does. Use when the user asks what their site should have, whether something is required, how to audit a URL, what's missing for agent readiness, or anything else where you'd otherwise be guessing at web best practice. Backs answers with primary sources and ships an MCP server with search, list, fetch, checklist, and audit tools.",
"url": "/.well-known/agent-skills/specification-website/SKILL.md",
"digest": "sha256:1506d0edc9a5ec36be2bd8c5054d4f3a9d85385d927e3bc3bc3121bcb9155205"
"digest": "sha256:4aa0455f9b9dc612881577d35b8cf72508a5a77c3d9664fe8dfb962545bfe3c2"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Query and apply The Website Specification — a platform-agnostic s

# specification.website

The Website Specification is a single source of truth for what a good website does. Ten categories, 169 pages, every item tagged with a status. It ships in three machine-readable forms: per-page Markdown, llms.txt / llms-full.txt, and an MCP server.
The Website Specification is a single source of truth for what a good website does. Ten categories, 170 pages, every item tagged with a status. It ships in three machine-readable forms: per-page Markdown, llms.txt / llms-full.txt, and an MCP server.

## When to use this skill

Expand Down
Binary file modified public/og-default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/og/checklist.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/og/spec.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/og/spec/performance.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/og/spec/performance/vary.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions src/content/changelog/2026-09-10-vary-header.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: "Added a page on the Vary header"
date: "2026-09-10"
type: added
relatedSlugs: [vary, cache-control, compression, markdown-source-endpoints]
---

`Vary` was mentioned in passing on half a dozen pages but never had one of its own, so it now gets [a page in performance](/spec/performance/vary/). It covers the part people get wrong — `Vary` extends the cache key rather than governing freshness — and the two opposite failures: negotiating on a header you did not list, and listing so many that the cache stops matching anything.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ summary: "Expose every documentation page's raw Markdown source at a predictable
status: recommended
order: 35
appliesTo: [all]
relatedSlugs: [llms-txt, llms-full-txt, machine-readable-formats, structured-data-for-agents, agent-readiness-overview, agent-skills-discovery, schemamap, conditional-requests, digest-fields]
relatedSlugs: [llms-txt, llms-full-txt, machine-readable-formats, structured-data-for-agents, agent-readiness-overview, agent-skills-discovery, schemamap, conditional-requests, vary, digest-fields]
updated: "2026-07-01T00:00:00.000Z"
sources:
- title: "RFC 9110 — HTTP Semantics §12 (Content Negotiation), §8.7 (Vary), §8.7.1 (Content-Location)"
Expand Down
4 changes: 2 additions & 2 deletions src/content/spec/performance/cache-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ summary: "Cache-Control tells browsers and CDNs how long to keep a response. Use
status: required
order: 50
appliesTo: [all]
relatedSlugs: [compression, core-web-vitals, no-vary-search, conditional-requests, compression-dictionary-transport]
relatedSlugs: [compression, core-web-vitals, vary, no-vary-search, conditional-requests, compression-dictionary-transport]
updated: "2026-08-01T00:00:00.000Z"
sources:
- title: "RFC 9111 — HTTP Caching"
Expand Down Expand Up @@ -78,7 +78,7 @@ Pair with a validator so revalidation is cheap — see [conditional requests](/s

**Per-user content — private.** Anything personalised needs `private` to prevent shared caches from leaking one user's data to another.

**Set `Vary` correctly.** If the response varies by `Accept-Encoding` or `Accept-Language`, set `Vary` accordingly. Missing `Vary` causes a CDN to serve gzip to a client that asked for brotli.
**Set [`Vary`](/spec/performance/vary/) correctly.** If the response varies by `Accept-Encoding` or `Accept-Language`, set `Vary` accordingly. Missing `Vary` causes a CDN to serve gzip to a client that asked for brotli.

**Survive origin failures — `stale-if-error`.** Defined in RFC 5861, this directive tells shared caches and browsers to keep serving the last good copy when a revalidation request fails — a 5xx from the origin, a timeout, or a connection error. It turns a backend outage into stale-but-working pages instead of error pages:

Expand Down
4 changes: 2 additions & 2 deletions src/content/spec/performance/compression.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ summary: "Compress text responses with brotli where supported, gzip everywhere e
status: required
order: 60
appliesTo: [all]
relatedSlugs: [cache-control, http3, conditional-requests, compression-dictionary-transport]
relatedSlugs: [cache-control, vary, http3, conditional-requests, compression-dictionary-transport]
updated: "2026-07-09T00:00:00.000Z"
sources:
- title: "RFC 7932 — Brotli Compressed Data Format"
Expand Down Expand Up @@ -59,7 +59,7 @@ Text resources — HTML, CSS, JavaScript, JSON, SVG, XML — compress to 20–30

**Pre-compress static assets.** For files that don't change (your bundled JS), compress at build time to maximum level (brotli quality 11, gzip level 9) and let the server serve the `.br` or `.gz` file directly. Runtime compression usually runs at level 5–6 for CPU reasons.

**Set `Vary: Accept-Encoding`.** Tells CDNs to keep a separate cache entry per encoding. Without it, gzip clients may receive a brotli body they can't decode.
**Set [`Vary: Accept-Encoding`](/spec/performance/vary/).** Tells CDNs to keep a separate cache entry per encoding. Without it, gzip clients may receive a brotli body they can't decode.

**Don't double-compress.** Images (JPEG, PNG, WebP, AVIF), video, fonts (WOFF2), and zip files are already compressed. Re-encoding wastes CPU and often grows the file.

Expand Down
2 changes: 1 addition & 1 deletion src/content/spec/performance/conditional-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ summary: "Send a validator — ETag or Last-Modified — on every cacheable resp
status: recommended
order: 52
appliesTo: [all]
relatedSlugs: [cache-control, compression, markdown-source-endpoints, stable-urls]
relatedSlugs: [cache-control, vary, compression, markdown-source-endpoints, stable-urls]
updated: "2026-06-09T11:00:00.000Z"
sources:
- title: "RFC 9110 §13 — Conditional Requests"
Expand Down
2 changes: 1 addition & 1 deletion src/content/spec/performance/no-vary-search.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ summary: "The `No-Vary-Search` response header tells browsers and caches that so
status: recommended
order: 55
appliesTo: [all]
relatedSlugs: [cache-control, canonical-url, speculation-rules, view-transitions, url-structure]
relatedSlugs: [cache-control, vary, canonical-url, speculation-rules, view-transitions, url-structure]
updated: "2026-07-09T00:00:00.000Z"
sources:
- title: "draft-ietf-httpbis-no-vary-search — The No-Vary-Search HTTP Caching Extension"
Expand Down
82 changes: 82 additions & 0 deletions src/content/spec/performance/vary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: "Vary — the cache key contract"
slug: vary
category: performance
summary: "Vary lists the request headers your server branched on when it chose this representation. Get it wrong and a cache hands one visitor's response to another; list too much and the cache stops working at all."
status: recommended
order: 51
appliesTo: [all]
relatedSlugs:
[
cache-control,
conditional-requests,
compression,
no-vary-search,
markdown-source-endpoints,
]
updated: "2026-09-10T00:00:00.000Z"
sources:
- title: "RFC 9110 §12.5.5 — Vary"
url: "https://www.rfc-editor.org/rfc/rfc9110#name-vary"
publisher: "IETF"
- title: "RFC 9111 §4.1 — Calculating Cache Keys with the Vary Header Field"
url: "https://www.rfc-editor.org/rfc/rfc9111#name-calculating-cache-keys-with"
publisher: "IETF"
- title: "MDN — Vary"
url: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Vary"
publisher: "MDN"
---

## What it is

`Vary` is a response header naming the request header fields your server consulted when it picked which representation to send:

```http
Content-Type: text/markdown; charset=utf-8
Vary: Accept, Accept-Encoding
```

That response can be reused only for requests whose `Accept` and `Accept-Encoding` match the original request, allowing semantics-preserving normalisation. If no stored variant matches, the cache must forward the request.

**`Vary` is not a sibling of `Cache-Control`, and it does not mean "this changes often".** That is the belief most people arrive with, and it is the source of nearly every mistake below. `Cache-Control` decides *whether and for how long* a response may be stored. `Vary` decides *which stored response may be handed back*. Adding `Vary` does not make a response fresher, shorter-lived, or less cacheable — it files it under a longer key. Which is why getting `Vary` wrong does not show up as stale content. It shows up as a cache serving one visitor the response that was built for a different one.

RFC 9111 §4.1 spells out the matching rule: a stored response is only reusable if, for every field named in its `Vary`, the new request's value matches the original request's. `Vary: *` never matches anything, so it means "never reuse", not "vary on everything".

## Why it matters

Content negotiation without `Vary` is broken by construction. Suppose the same URL returns HTML to a browser and Markdown to an agent that sent `Accept: text/markdown`. If the response omits `Vary: Accept`, the first representation a shared cache stores is the one it returns to everybody — so either agents get HTML they asked not to receive, or visitors get raw Markdown in the browser. The origin is negotiating perfectly the whole time; only the cache in front of it is wrong, which is exactly why this survives local testing and appears in production.

The same failure with `Accept-Encoding` is uglier: a client that cannot decode Brotli is handed a Brotli body and renders nothing.

The opposite failure is cache fragmentation. Each additional field can increase the number of variants stored for a URL. Fields with many distinct values, such as `User-Agent`, can divide requests among many entries and reduce reuse. Keep the list limited to fields that affect the response, and measure the cache hit rate. RFC 9110 §12.5.5 discusses the performance cost of expanding the cache key; it does not set a maximum number of fields.

## How to implement

**List exactly what you branched on — no more, no less.** If the server chose this body by reading `Accept-Encoding`, list `Accept-Encoding`. If it did not read `Accept-Language`, do not list it because the site happens to be translated.

**Set it on every representation, including the default.** A cache that stored the HTML response *without* `Vary: Accept` will happily reuse it for a request that asked for Markdown. Both branches of a negotiation need the header, not just the interesting one.

**Verify how your cache normalises values.** RFC 9111 §4.1 permits normalisation that preserves a field's semantics, including reordering values where order is insignificant. `Accept-Encoding: gzip, br` and `Accept-Encoding: br, gzip` therefore need not create different cache entries. Whether your cache merges them depends on its implementation. If you configure a smaller set of cache-key values, make sure that requests mapped to the same key can safely receive the same response.

**Prefer distinct URLs when the representations differ substantially.** `Vary` earns its place for encodings and for format mirrors of the same document. It is a poor way to serve different languages — give each locale its own URL and wire them together with [hreflang](/spec/i18n/hreflang/), so the content is linkable, shareable, and indexable. See [international URL structure](/spec/i18n/international-url-structure/).

**Treat `Vary: Cookie` on a public page as a bug report.** It nearly always means personalisation has leaked into a document that should have been cacheable. Either mark the response `private`, or move the personal fragment to a separate client-side request and keep the page shared.

**Avoid `Vary: User-Agent`.** The one context where it is conventionally sanctioned is serving separate mobile markup from a single URL, and it costs you most of your shared caching to do it. Responsive markup removes the need entirely.

This site ships `Vary: Accept` on every spec page, because each canonical URL returns HTML or Markdown depending on what the client asked for — see [per-page Markdown source endpoints](/spec/agent-readiness/markdown-source-endpoints/).

## Common mistakes

- Negotiating on a header that is not listed in `Vary`. The most damaging error here, and invisible until a cache is in front of you.
- Adding `Vary` to the negotiated response but not to the default one.
- `Vary: User-Agent` on a responsive site. Nothing about the response actually varies; only the hit rate does.
- Assuming the CDN adds `Vary: Accept-Encoding` for you. Many normalise encoding themselves and strip or rewrite the header — verify what leaves the edge, not what leaves the origin.
- Reading `Vary: *` as "varies on everything". It means the response can never be reused from cache.
- Listing fields defensively "in case we negotiate on them later". Every unused field is a permanent tax on the hit rate.

## Verification

- `curl -sI -H 'Accept: text/markdown' https://example.com/page/` and the same request without the header: the `Content-Type` should differ and *both* responses should carry `Vary: Accept`.
- Request the same URL through your CDN with `Accept-Encoding: gzip` and `Accept-Encoding: br`, in both orders. Each response must use an encoding the client accepts; an unencoded response can also be valid. Confirm that cache reuse or edge recompression never serves an incompatible encoding, rather than assuming the CDN stores two separate bodies.
- Scan your responses for `Vary` values listing more than two fields. Each one should trace to a branch that genuinely exists in the server code.