Skip to content

feat(docs): serve Markdown to clients that ask for it - #12596

Open
jd wants to merge 1 commit into
devs/jd/agentic-readiness/publish-api-desc-machines-look--1b625f96from
devs/jd/agentic-readiness/serve-markdown-clients-ask--52f9d2a1
Open

feat(docs): serve Markdown to clients that ask for it#12596
jd wants to merge 1 commit into
devs/jd/agentic-readiness/publish-api-desc-machines-look--1b625f96from
devs/jd/agentic-readiness/serve-markdown-clients-ask--52f9d2a1

Conversation

@jd

@jd jd commented Aug 28, 2026

Copy link
Copy Markdown
Member

Every page already ships its Markdown source at <path>.md, which is what the
"View as Markdown" button and llms.txt link to. But the convention agents
reach for first is to request the page's own URL with Accept: text/markdown
(https://acceptmarkdown.com), and that returned HTML.

The site is a static build, so a Cloudflare Pages middleware is the only layer
that sees request headers. It rewrites to the .md twin when Markdown is asked
for, answers 404s in the format that was requested, and merges Accept into
Vary on both variants — without it a CDN would hand whichever variant it
cached first to everyone, which is the failure this is most likely to produce
in production and the least likely to be noticed. The Vary goes on 304
responses too, since those are exactly the ones a cache is about to act on.

Markdown is served only when text/markdown is named explicitly and not
outranked by text/html. Browsers send text/html,...,*/*;q=0.8, so honouring
wildcards would serve raw Markdown to every human visitor; the tests pin the
real Chrome and Firefox Accept headers against that.

HTML is served as the fallback only on a 404 from the .md route, not on
anything that is merely not ok: 304 Not Modified is the normal answer to a
client revalidating Markdown it already holds, and treating it as a missing
page answered it with HTML.

Three things about adding a root _middleware needed care, all three verified
against the real Pages runtime with wrangler pages dev dist:

Cloudflare documents that _redirects are not applied to requests served by
Functions, and this repository has 99 of them. next() does still route
through the asset server, so every redirect, the _headers rules, the .md
routes and the static 404 behave exactly as before.

next() is called with an explicit request everywhere. A bare next() is
documented as forwarding the original request, but once the middleware has
asked for the Markdown twin the runtime forwards that request again — so
/api and /cli, which are built from src/pages/ and have no .md, were
answered with the Markdown 404 instead of their own HTML. The test double
refuses a call with no request so this cannot come back.

A root middleware otherwise turns every request into a Worker invocation,
assets included — roughly thirty per page view, none of which can be
negotiated. _routes.json excludes the bundle, the search index, the
OpenGraph images and the static files by name, so only page requests reach the
Function.

Also ignore .wrangler/ — running the Pages runtime locally drops generated
bundles there, and eslint linted them.

Depends-On: #12595

@jd

jd commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

This pull request is part of a Mergify stack:

# Pull Request Link
1 fix(seo): restore the homepage OpenGraph image #12593
2 fix(a11y): stop docset grids skipping a heading level #12594
3 feat(docs): publish the API description where machines look for it #12595
4 feat(docs): serve Markdown to clients that ask for it #12596 👈
5 feat(docs): tell agents what Mergify is for in llms.txt #12597
6 feat(seo): describe the site with schema.org JSON-LD #12598
7 feat(docs): point a lost agent somewhere useful from the 404 #12599

@mergify

mergify Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🔴 3 of 7 protections blocking · waiting on 👀 reviews and ⛓️ dependency

Protection Waiting on
🔴 ⛓️ Depends-On Requirements ⛓️ dependency
🔴 👀 Review Requirements 👀 reviews
🔴 🔎 Reviews 👀 reviews
🟢 🤖 Continuous Integration
🟢 Enforce conventional commit
🟢 📕 PR description
🟢 🚦 Auto-queue

🔴 ⛓️ Depends-On Requirements

Waiting for

This rule is failing.

Requirement based on the presence of Depends-On in the body of the pull request

🔴 👀 Review Requirements

Waiting for

  • #approved-reviews-by >= 2
This rule is failing.
  • any of:
    • #approved-reviews-by >= 2
    • author = dependabot[bot]
    • author = renovate[bot]
    • all of:
      • author = mergify-ci-bot
      • -head ~= ^docs-agent/

🔴 🔎 Reviews

Waiting for

  • #review-requested = 0
This rule is failing.
  • #review-requested = 0
  • #changes-requested-reviews-by = 0
  • #review-threads-unresolved = 0

Show 4 satisfied protections

🟢 🤖 Continuous Integration

  • all of:
    • check-success = build
    • check-success = lint
    • check-success = test
    • any of:
      • check-success = test-broken-links
      • label = ignore-broken-links
    • any of:
      • check-success=Cloudflare Pages
      • -head-repo-full-name~=^Mergifyio/

🟢 Enforce conventional commit

Make sure that we follow https://www.conventionalcommits.org/en/v1.0.0/

  • title ~= ^(fix|feat|internal|docs|style|refactor|perf|test|build|ci|chore|revert|ui)(?:\(.+\))?!?:

🟢 📕 PR description

  • body ~= (?ms:.{48,})

🟢 🚦 Auto-queue

When all merge protections are satisfied, this pull request will be queued automatically.

@mergify
mergify Bot requested a review from a team August 28, 2026 13:31
Copilot AI lite review requested due to automatic review settings August 28, 2026 13:33
@jd
jd force-pushed the devs/jd/agentic-readiness/serve-markdown-clients-ask--52f9d2a1 branch from 1171c35 to 73f5d6b Compare August 28, 2026 13:33
@jd

jd commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Revision history

# Type Changes Reason Date
1 initial 1171c35 2026-08-28 13:33 UTC
2 content 1171c35 → 73f5d6b Scope the middleware with _routes.json: a root _middleware makes every request a Worker invocation, assets included (~30 per page view), none of which can be negotiated. Excludes the bundle, search i… 2026-08-28 13:33 UTC
3 content 73f5d6b → 80ec774 Fix the 304 fallback copilot found on #12596, plus a next(request) bug it surfaced; add middleware tests 2026-08-28 14:37 UTC
4 rebase 80ec774 → ecb955f (rebase only) 2026-08-28 15:09 UTC

@mergify
mergify Bot had a problem deploying to Mergify Merge Protections August 28, 2026 13:34 Failure

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Cloudflare Pages middleware-based content negotiation so clients explicitly requesting text/markdown receive the existing per-page *.md “twin”, while keeping CDN caching correct via Vary: Accept and avoiding Worker invocations for most static assets.

Changes:

  • Introduces prefersMarkdown() + isNegotiablePage() utilities (with Vitest coverage) to drive safe Accept-header negotiation.
  • Adds Pages middleware to rewrite negotiable page requests to their *.md twin, return Markdown-formatted 404s when appropriate, and merge Accept into Vary for negotiated media types.
  • Adds <link rel="alternate" type="text/markdown"> in page head, and introduces _routes.json plus lint/format ignores for local Wrangler artifacts.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/util/acceptMarkdown.ts Implements Accept parsing + policy for when to serve Markdown.
src/util/acceptMarkdown.test.ts Unit tests covering key negotiation scenarios and asset/page detection.
src/components/HeadSEO.astro Publishes the Markdown twin via an alternate link tag.
public/_routes.json Limits Pages Function routing to page requests by excluding major static asset paths.
functions/_middleware.ts Rewrites requests to *.md when appropriate and sets Vary: Accept on negotiated responses.
eslint.config.js Ignores local .wrangler/ artifacts for linting.
biome.json Excludes .wrangler/ from Biome formatting/linting.
.gitignore Ignores local Wrangler build artifacts.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread functions/_middleware.ts
@jd
jd marked this pull request as ready for review August 28, 2026 14:08
@jd
jd force-pushed the devs/jd/agentic-readiness/serve-markdown-clients-ask--52f9d2a1 branch from 73f5d6b to 80ec774 Compare August 28, 2026 14:37
@mergify
mergify Bot had a problem deploying to Mergify Merge Protections August 28, 2026 14:37 Failure
Every page already ships its Markdown source at `<path>.md`, which is what the
"View as Markdown" button and `llms.txt` link to. But the convention agents
reach for first is to request the page's own URL with `Accept: text/markdown`
(https://acceptmarkdown.com), and that returned HTML.

The site is a static build, so a Cloudflare Pages middleware is the only layer
that sees request headers. It rewrites to the `.md` twin when Markdown is asked
for, answers 404s in the format that was requested, and merges `Accept` into
`Vary` on both variants — without it a CDN would hand whichever variant it
cached first to everyone, which is the failure this is most likely to produce
in production and the least likely to be noticed. The `Vary` goes on `304`
responses too, since those are exactly the ones a cache is about to act on.

Markdown is served only when `text/markdown` is named explicitly and not
outranked by `text/html`. Browsers send `text/html,...,*/*;q=0.8`, so honouring
wildcards would serve raw Markdown to every human visitor; the tests pin the
real Chrome and Firefox Accept headers against that.

HTML is served as the fallback only on a `404` from the `.md` route, not on
anything that is merely not `ok`: `304 Not Modified` is the normal answer to a
client revalidating Markdown it already holds, and treating it as a missing
page answered it with HTML.

Three things about adding a root `_middleware` needed care, all three verified
against the real Pages runtime with `wrangler pages dev dist`:

Cloudflare documents that `_redirects` are not applied to requests served by
Functions, and this repository has 99 of them. `next()` does still route
through the asset server, so every redirect, the `_headers` rules, the `.md`
routes and the static 404 behave exactly as before.

`next()` is called with an explicit request everywhere. A bare `next()` is
documented as forwarding the original request, but once the middleware has
asked for the Markdown twin the runtime forwards *that* request again — so
`/api` and `/cli`, which are built from `src/pages/` and have no `.md`, were
answered with the Markdown 404 instead of their own HTML. The test double
refuses a call with no request so this cannot come back.

A root middleware otherwise turns every request into a Worker invocation,
assets included — roughly thirty per page view, none of which can be
negotiated. `_routes.json` excludes the bundle, the search index, the
OpenGraph images and the static files by name, so only page requests reach the
Function.

Also ignore `.wrangler/` — running the Pages runtime locally drops generated
bundles there, and eslint linted them.

Change-Id: I52f9d2a1c70248b1406412129a633cc3bee219de
@mergify
mergify Bot had a problem deploying to Mergify Merge Protections August 28, 2026 15:10 Failure
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants