This site has talked to AI agents for a while. Since July 2025 it has published an A2A agent card, with JSON-RPC skills to list, fetch and search posts, and referrals from ChatGPT and Perplexity followed. I tightened its discovery in January. Every post carries schema.org JSON-LD. There is a JSON Feed with agent extensions. Those serve agents that know the site exists and know how to ask.
Most agents do not ask. They fetch a URL on someone’s behalf and read whatever comes back, and an agent pays for every token it reads.
I measured one post. As HTML, “One Control Point” is 158 KB: navigation, scripts, styles, the footer. As markdown it is 17 KB. The agent wants the 17 KB, and until today it had to dig it out of the 158.

Cloudflare shipped a feature for exactly this, Markdown for Agents: send Accept: text/markdown and the edge converts the page for you. It is becoming the plain-language way to talk to agents, the one any client that can make an HTTP request already speaks. It is on Cloudflare’s Pro plan and up. My zone is on the free plan, and the site is hosted on Vercel anyway. So I built it into the site, next to the A2A and JSON-LD it already had.

What an agent gets now
- Every post as plain markdown at its own URL: add
.mdto any post address. - A request for a post with
Accept: text/markdownlands on that markdown copy. /llms.txt, a map of the site for agents in the llms.txt format: the last two years of posts up front, the archive in an Optional section an agent with little context can skip./llms-full.txt, the full text of those recent posts in one file.- Markdown copies of the about, talks and resume pages, so a question about who I am gets a clean answer too.
- A
Content-Signalline in robots.txt and on every markdown response, saying search, AI answers and training are all welcome, with attribution.
The markdown is built from the post source, not scraped from the rendered page. Interactive figures become their descriptions. Diagrams stay diagrams. Styled boxes become plain lists.
Why it is a redirect, not a rewrite
The obvious design is a rewrite: same URL, different body depending on what the client asks for. On my site that rule never runs.
My posts are prebuilt HTML files, and Vercel serves a file that exists before it looks at any rewrite. Vercel’s documentation says the filesystem takes precedence over rewrites. A redirect is different: a redirect with a header condition runs before the file check. So a request for a post that asks for markdown, through HTTP’s own content negotiation header, gets a 307 to the .md copy, and the copy answers 200.
It costs one hop, and agents follow same-site redirects. It also buys something. The HTML and the markdown each have their own URL, so no cache can ever hand a person the markdown or an agent the HTML.

One rulebook, two hosts
I also wanted the same code to run on Cloudflare Workers, so I am not tied to one host.
The site’s redirects and headers live in one file, vercel.json: 146 redirects, many of them legacy URLs that search engines still send people to. Copying them into a second format for Cloudflare would be a promise to keep two lists in sync forever. Instead, a small Worker that runs before any static file is served reads the same file and applies it, using the same path-matching syntax Vercel uses.
Then I stopped trusting that it matched. A script requests every one of those 146 redirects from the Cloudflare build and from production, and compares the status code and destination. All 146 match.

Another script checks the agent features against any deployment in about a minute, and it now runs after every production deploy.
Three traps no local test caught
Everything passed on my laptop. Three things only showed up in production. It is not the first time: Vercel’s production caching once swallowed my JSON-RPC POSTs.
The site was shipping development builds of its JavaScript. My Vercel project had NODE_ENV set to "production\n", with a trailing newline, for over a year. The build tooling decides whether a build is a production build by comparing that variable to "production", exactly. The newline made production look like development. The proof was in the shipped bundle: production carried debugging code that my local build did not. I deleted the variable, redeployed, and the production bundle came back byte for byte identical to my local build.
On Vercel, the server thinks every request comes from localhost. I gated two debugging endpoints to local requests only. They kept answering in production. Astro trusts the request’s host only for domains you list in its configuration, and without that list it reports localhost, so my check passed for everyone. The fix was to ask the platform directly, the way the rest of the code already did.
The feed was serving source code. My JSON feed, the one every page advertises to agents, had been putting the raw post source in its HTML field since I added it, three days before the A2A guide, over a year ago: import statements, style blocks and all. Every feed reader and agent that read it got code instead of writing. It now carries the same cleaned text as the markdown copies.
The common thread: each of these passed every check I could run locally. What found them was asking production directly and comparing it with what I expected.
If you want this on your own site
- Generate a markdown copy of every page you want read, from the source, and link it from the page head.
- If your pages are prebuilt files, test whether your host runs rewrites before the filesystem. If it does not, use a redirect.
- Publish
/llms.txtwith the recent work first and the archive under Optional. - Write one check that compares a new deployment with production, and run it after every deploy.
- Look at your production environment variables. All of them. Mine had been wrong for a year.
Which of these would your site fail first?