Part of epic #3306.
Problem
Agents parse markdown far more reliably than they parse our rendered HTML (heavy JS, panels, ARIA). Cloudflare's Markdown for Agents spec describes the response-side contract: if the client sends Accept: text/markdown, return a markdown representation of the page with Content-Type: text/markdown. Browsers still get HTML.
Fix
Implement per-route Accept-based content negotiation for our HTML surfaces. For any route that returns HTML today:
- If the request has
Accept: text/markdown (and that is higher priority than text/html), return a markdown rendering of the same logical content.
- Otherwise, default to HTML.
- Response MUST set
Content-Type: text/markdown; charset=utf-8 on the markdown branch. If/when we tokenise, also emit x-markdown-tokens.
Vary: Accept on all responses to prevent CDN cache poisoning.
Which routes are in scope
Home (/), country pages (/country/[iso] if those exist), /docs/*, /pro, /about. Panel-heavy dashboard routes probably return "dashboard overview + link to data API" rather than trying to serialise widgets to markdown — keep it informative, not mechanical.
Implementation
WorldMonitor ships as a Vite + hybrid app; there isn't a single server-rendered Next-style layout per route. Two realistic paths:
- Build-time markdown siblings. Generate a markdown version of each static route at build time (from the canonical source, e.g. MDX/CMS), then have
middleware.ts rewrite the request to the .md sibling when Accept: text/markdown wins. Works today for static content; useless for panels.
- Edge function that fetches and converts. An edge fn that takes a URL, pulls the HTML, strips chrome, pipes through
turndown or similar. Expensive per-request; consider caching.
Prefer (1) for docs / static pages, and for dynamic pages accept that the markdown branch might return a summary + "use the API (<catalog link>) for structured data" message instead of a full conversion.
Acceptance criteria
References
Part of epic #3306.
Problem
Agents parse markdown far more reliably than they parse our rendered HTML (heavy JS, panels, ARIA). Cloudflare's Markdown for Agents spec describes the response-side contract: if the client sends
Accept: text/markdown, return a markdown representation of the page withContent-Type: text/markdown. Browsers still get HTML.Fix
Implement per-route
Accept-based content negotiation for our HTML surfaces. For any route that returns HTML today:Accept: text/markdown(and that is higher priority thantext/html), return a markdown rendering of the same logical content.Content-Type: text/markdown; charset=utf-8on the markdown branch. If/when we tokenise, also emitx-markdown-tokens.Vary: Accepton all responses to prevent CDN cache poisoning.Which routes are in scope
Home (
/), country pages (/country/[iso]if those exist),/docs/*,/pro,/about. Panel-heavy dashboard routes probably return "dashboard overview + link to data API" rather than trying to serialise widgets to markdown — keep it informative, not mechanical.Implementation
WorldMonitor ships as a Vite + hybrid app; there isn't a single server-rendered Next-style layout per route. Two realistic paths:
middleware.tsrewrite the request to the.mdsibling whenAccept: text/markdownwins. Works today for static content; useless for panels.turndownor similar. Expensive per-request; consider caching.Prefer (1) for docs / static pages, and for dynamic pages accept that the markdown branch might return a summary + "use the API (
<catalog link>) for structured data" message instead of a full conversion.Acceptance criteria
curl -H 'Accept: text/markdown' https://worldmonitor.app/returns 200 withContent-Type: text/markdownand a sensible markdown body.curl -H 'Accept: text/html' https://worldmonitor.app/returns HTML as today.Vary: Acceptpresent on both.isitagentready.com"Markdown for Agents" check passes.References