---
title: "My Blog Spoke A2A. Now It Speaks Markdown Too"
description: "This site already talked to agents through A2A and JSON-LD. But an agent that simply fetches a post still downloaded 158 KB of HTML to read 17 KB of writing. So I added Cloudflare's convention, markdown on request, made it run on Vercel and Cloudflare from one set of rules, and found three traps on the way that no local test caught."
author: "Colin McNamara"
tags: ["Agents","Web","Vercel","Cloudflare","Astro"]
image: "https://colinmcnamara.com/images/blog/markdown-for-agents/social-card.png"
date: 2026-10-12
url: https://colinmcnamara.com/blog/my-blog-spoke-a2a-now-it-speaks-markdown
---

# My Blog Spoke A2A. Now It Speaks Markdown Too

This site has talked to AI agents for a while. [Since July 2025](https://colinmcnamara.com/blog/how-to-implement-a2a-protocol-on-your-blog) it has published an [A2A](https://a2a-protocol.org/latest/specification/) agent card, with JSON-RPC skills to list, fetch and search posts, and [referrals from ChatGPT and Perplexity followed](https://colinmcnamara.com/blog/a2a-traffic-observations). I [tightened its discovery](https://colinmcnamara.com/blog/a2a-protocol-improvements-agent-discovery) in January. Every post carries [schema.org](https://schema.org/BlogPosting) JSON-LD. There is a [JSON Feed](https://www.jsonfeed.org/version/1.1/) 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.

![Two bars compare the same post: the HTML version is 158 KB and the markdown version is 17 KB, a bar about one ninth as long. The markdown copy is about 89% smaller.](https://colinmcnamara.com/images/blog/markdown-for-agents/size-bars.png)

Cloudflare shipped a feature for exactly this, [Markdown for Agents](https://blog.cloudflare.com/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](https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/). 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.

![Two columns of cards. Left, agents that know how to ask: an A2A agent card with JSON-RPC skills to list, get and search posts, schema.org BlogPosting JSON-LD on every post, and a JSON Feed. Right, marked new, agents that just fetch a URL: llms.txt as a map of the site, a .md copy of each post, an Accept: text/markdown header that returns that copy, and a Content-Signal header welcoming search, AI answers and training.](https://colinmcnamara.com/images/blog/markdown-for-agents/two-ways.png)

## What an agent gets now

- Every post as plain markdown at its own URL: add `.md` to any post address.
- A request for a post with `Accept: text/markdown` lands on that markdown copy.
- `/llms.txt`, a map of the site for agents in the [llms.txt](https://llmstxt.org/) 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-Signal`](https://contentsignals.org/) line in [robots.txt](https://www.rfc-editor.org/rfc/rfc9309.html) 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](https://vercel.com/docs/project-configuration/vercel-json) 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](https://www.rfc-editor.org/rfc/rfc9110.html) 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.

![Two request lanes. A browser asking for /blog/post with Accept: text/html matches no redirect rule, the file check finds the prebuilt page, and it gets 200 HTML. An agent asking with Accept: text/markdown matches a redirect rule on the Accept header, gets a 307 to /blog/post.md, and receives 200 text/markdown. A footnote says rewrites run after the file check, so a rewrite never sees a prebuilt page.](https://colinmcnamara.com/images/blog/markdown-for-agents/request-flow.png)

## 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](https://developers.cloudflare.com/workers/static-assets/routing/worker-script/) 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.

![One vercel.json holding 146 redirects, headers and the markdown rule feeds two hosts: Vercel reads it natively, and a Cloudflare Worker applies it with URLPattern. A parity check shows 146 of 146 redirects match production.](https://colinmcnamara.com/images/blog/markdown-for-agents/one-rulebook.png)

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](https://colinmcnamara.com/blog/vercel-isr-caching-post-requests-json-rpc).

**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](https://docs.astro.build/en/reference/configuration-reference/), 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](https://colinmcnamara.com/blog/how-to-implement-a2a-protocol-on-your-blog), 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

1. Generate a markdown copy of every page you want read, from the source, and link it from the page head.
2. If your pages are prebuilt files, test whether your host runs rewrites before the filesystem. If it does not, use a redirect.
3. Publish `/llms.txt` with the recent work first and the archive under Optional.
4. Write one check that compares a new deployment with production, and run it after every deploy.
5. Look at your production environment variables. All of them. Mine had been wrong for a year.

Which of these would your site fail first?
