diff --git a/functions/_middleware.ts b/functions/_middleware.ts index 7f979b8b..0aac2ebd 100644 --- a/functions/_middleware.ts +++ b/functions/_middleware.ts @@ -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 @@ -101,8 +133,12 @@ export const onRequest: PagesFunction = 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"; diff --git a/public/og-default.png b/public/og-default.png index 120c1139..5f16bc6d 100644 Binary files a/public/og-default.png and b/public/og-default.png differ diff --git a/public/og/checklist.png b/public/og/checklist.png index 13f11cf4..7bc84bff 100644 Binary files a/public/og/checklist.png and b/public/og/checklist.png differ diff --git a/public/og/spec.png b/public/og/spec.png index 23cb9f93..9a298a4f 100644 Binary files a/public/og/spec.png and b/public/og/spec.png differ diff --git a/public/og/spec/resilience.png b/public/og/spec/resilience.png index 3c5a3377..1b9875ec 100644 Binary files a/public/og/spec/resilience.png and b/public/og/spec/resilience.png differ diff --git a/public/og/spec/resilience/deprecation-and-sunset.png b/public/og/spec/resilience/deprecation-and-sunset.png new file mode 100644 index 00000000..0964d101 Binary files /dev/null and b/public/og/spec/resilience/deprecation-and-sunset.png differ diff --git a/src/content/changelog/2026-07-08-deprecation-and-sunset.md b/src/content/changelog/2026-07-08-deprecation-and-sunset.md new file mode 100644 index 00000000..78546dc3 --- /dev/null +++ b/src/content/changelog/2026-07-08-deprecation-and-sunset.md @@ -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. diff --git a/src/content/spec/resilience/deprecation-and-sunset.md b/src/content/spec/resilience/deprecation-and-sunset.md new file mode 100644 index 00000000..0a857de0 --- /dev/null +++ b/src/content/spec/resilience/deprecation-and-sunset.md @@ -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: ; rel="successor-version", + ; 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`. diff --git a/src/content/spec/resilience/error-pages.md b/src/content/spec/resilience/error-pages.md index dc015916..e31cc9f3 100644 --- a/src/content/spec/resilience/error-pages.md +++ b/src/content/spec/resilience/error-pages.md @@ -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" diff --git a/src/content/spec/seo/redirects.md b/src/content/spec/seo/redirects.md index 98e04c42..5ba1d597 100644 --- a/src/content/spec/seo/redirects.md +++ b/src/content/spec/seo/redirects.md @@ -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"