Skip to content
Merged
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
40 changes: 38 additions & 2 deletions functions/_middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,38 @@ type Env = {

const SPEC_PAGE = /^\/spec\/([^/]+)\/([^/]+)\/?$/;

// Retired well-known URI. `/.well-known/ai.txt` was removed on 2026-05-29;
// the convention proved defunct. Rather than a bare 404 we serve a 410 Gone
// that records the retirement in machine-readable form: Deprecation (RFC 9745)
// carries the date it was deprecated, Sunset (RFC 8594) the removal instant,
// and a rel="deprecation" link points to the human explanation. RFC 8594
// explicitly covers this post-sunset 410 case. Worked example for
// /spec/resilience/deprecation-and-sunset/.
const AI_TXT_PATH = "/.well-known/ai.txt";
const AI_TXT_DEPRECATION = "@1780012800"; // 2026-05-29T00:00:00Z
const AI_TXT_SUNSET = "Fri, 29 May 2026 00:00:00 GMT";
const AI_TXT_DOCS =
"https://specification.website/spec/resilience/deprecation-and-sunset/";

function goneAiTxt(): Response {
const body =
"410 Gone\n\n" +
"/.well-known/ai.txt was retired on 2026-05-29. The convention is defunct;\n" +
"express AI-crawler preferences via robots.txt and content signals instead.\n" +
`See ${AI_TXT_DOCS}\n`;
return new Response(body, {
status: 410,
statusText: "Gone",
headers: {
"Content-Type": "text/plain; charset=utf-8",
Deprecation: AI_TXT_DEPRECATION,
Sunset: AI_TXT_SUNSET,
Link: `<${AI_TXT_DOCS}>; rel="deprecation"; type="text/html"`,
"Cache-Control": "public, max-age=3600",
},
});
}

function prefersMarkdown(accept: string): boolean {
// We treat the request as "wants markdown" only when text/markdown is
// explicitly named. Browsers default to text/html and don't hit this
Expand Down Expand Up @@ -101,8 +133,12 @@ export const onRequest: PagesFunction<Env> = async (context) => {
let response: Response;
let repr = "passthrough";

// Site root: agents asking for Markdown get llms.txt (the site index).
if (url.pathname === "/" || url.pathname === "") {
// Retired well-known URI: serve a 410 Gone with Deprecation/Sunset headers.
if (url.pathname === AI_TXT_PATH) {
response = goneAiTxt();
repr = "gone";
// Site root: agents asking for Markdown get llms.txt (the site index).
} else if (url.pathname === "/" || url.pathname === "") {
if (wantsMarkdown) {
response = await serveAsMarkdown(env, url, "/llms.txt");
repr = "markdown";
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/resilience.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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-07-08-deprecation-and-sunset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Added a page on Deprecation and Sunset headers
date: "2026-07-08"
type: added
relatedSlugs: [deprecation-and-sunset]
---

Added a page on [Deprecation and Sunset](/spec/resilience/deprecation-and-sunset/) (RFC 9745 + RFC 8594), the response headers that announce a resource is going away, when, and where to go next, so clients migrate before it breaks. The site now ships a worked example: the retired `/.well-known/ai.txt` returns `410 Gone` carrying `Deprecation`, `Sunset`, and a `rel="deprecation"` link.
67 changes: 67 additions & 0 deletions src/content/spec/resilience/deprecation-and-sunset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: "Deprecation and Sunset"
slug: deprecation-and-sunset
category: resilience
summary: "When you retire an endpoint, announce it in machine-readable form: the Deprecation and Sunset response headers tell clients it is going away, when, and where to go next — so integrations migrate before anything breaks."
status: optional
order: 55
appliesTo: [all]
relatedSlugs: [error-pages, redirects, maintenance-pages, stable-urls]
updated: "2026-07-08T00:00:00.000Z"
sources:
- title: "RFC 9745 — The Deprecation HTTP Response Header Field"
url: "https://www.rfc-editor.org/rfc/rfc9745"
publisher: "IETF"
- title: "RFC 8594 — The Sunset HTTP Header Field"
url: "https://www.rfc-editor.org/rfc/rfc8594"
publisher: "IETF"
- title: "IANA — Link Relations Registry"
url: "https://www.iana.org/assignments/link-relations/link-relations.xhtml"
publisher: "IANA"
---

## What it is

When you retire an HTTP endpoint — an old API version, a moved feed, a well-known URI you no longer serve — two response headers let you announce it in machine-readable form so clients migrate before anything breaks.

- **`Deprecation`** (RFC 9745) marks a resource as deprecated. Its value is a date: `Deprecation: @1717200000`. A date in the past means it _was_ deprecated then; a future date means it _will_ be. The resource still works — this is advance notice.
- **`Sunset`** (RFC 8594) gives the date the resource is expected to stop responding: `Sunset: Tue, 30 Jun 2026 23:59:59 GMT`. It should be a future date while the resource is live; once it passes, requests typically return `410 Gone`.

The `Sunset` date **must not be earlier** than the `Deprecation` date. Pair either header with a `Link`: `rel="deprecation"` points to human documentation about the change, `rel="sunset"` to a retirement or migration policy, and `rel="successor-version"` to the replacement.

## Why it matters

- **Clients and agents find out on their own.** A consumer hitting the endpoint reads the date and the link, and schedules migration — no out-of-band emails or guesswork.
- **It turns a silent break into a planned one.** Without these headers an endpoint simply vanishes one day and every integration fails at once.
- **The signal is auditable.** Anyone can `curl -I` the endpoint and see when it is going away and where to go next.
- **It complements a real status code.** A retired URL should still return the right status (`404`/`410`) and, where a replacement exists, a [redirect](/spec/seo/redirects/); these headers add the _when_ and _why_.

## How to implement

While the resource is still live, emit the headers and keep serving it normally:

```
Deprecation: @1717200000
Sunset: Tue, 30 Jun 2026 23:59:59 GMT
Link: <https://api.example.com/v2/orders>; rel="successor-version",
<https://developer.example.com/deprecation/orders-v1>; rel="deprecation"
```

Announce as early as you can — the point is lead time. Do not start failing before the Sunset date. Once the resource is genuinely gone, return `410 Gone` (or `404`); you may keep the `Sunset` header to record when it went, plus a `rel="deprecation"` link so latecomers still find the explanation. RFC 8594 explicitly covers this post-sunset `410` case.

This site ships a worked example. `/.well-known/ai.txt` was retired, and rather than a bare 404 it returns `410 Gone` carrying `Deprecation`, `Sunset`, and a `rel="deprecation"` link back to this page (see `functions/_middleware.ts`). The `ai.txt` convention itself proved defunct — express AI-crawler preferences via [robots.txt](/spec/seo/robots-txt/) and content signals instead.

## Common mistakes

- A `Sunset` earlier than `Deprecation`. Invalid per RFC 9745.
- Emitting `Deprecation` but never a `Sunset` or a successor link — consumers learn it is deprecated but not when it dies or what to use instead.
- Failing the resource _before_ the announced Sunset. Deprecation is a promise to keep working until then.
- Using a bare `Deprecation: true`. The value is a structured-field date, not a boolean.

## Verification

```
curl -sI https://api.example.com/v1/orders | grep -iE 'deprecation|sunset|link'
```

Confirm `Deprecation` carries a date, `Sunset` is not earlier, and a `rel="deprecation"` or `rel="successor-version"` link is present. On a fully retired URL, confirm the status is `410`/`404`, not `200`.
2 changes: 1 addition & 1 deletion src/content/spec/resilience/error-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ summary: "Custom error pages must return the correct HTTP status code, explain w
status: required
order: 10
appliesTo: [all]
relatedSlugs: [soft-404, monitoring-uptime, maintenance-pages]
relatedSlugs: [soft-404, monitoring-uptime, maintenance-pages, deprecation-and-sunset]
updated: "2026-05-29T18:54:03.000Z"
sources:
- title: "RFC 9110 — HTTP Semantics: 404 Not Found"
Expand Down
2 changes: 1 addition & 1 deletion src/content/spec/seo/redirects.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ summary: "HTTP redirects send a client from one URL to another. Use 301 or 308 f
status: required
order: 60
appliesTo: [all]
relatedSlugs: [url-structure, canonical-url, soft-404]
relatedSlugs: [url-structure, canonical-url, soft-404, deprecation-and-sunset]
updated: "2026-05-29T09:13:20.000Z"
sources:
- title: "RFC 9110 — HTTP Semantics, redirection 3xx"
Expand Down
Loading