If you ran a URL through the Agent Runtime Readiness audit and the third check came back amber, you saw this exact line:
Host did not return Markdown content when Accept: text/markdown was requested. Enable Cloudflare Markdown for Agents or implement content negotiation at your origin.
The audit's first suggestion only helps if your site sits behind Cloudflare on a Pro or Business plan (covered in detail in the original post). If you're on Netlify, you're on the second path — origin content negotiation. Netlify's answer is Edge Functions, and the implementation is short.
Why Edge Functions and not Netlify Functions
Netlify has two serverless layers. Netlify Functions (the older netlify/functions/*) run in a regional Lambda and are best for proxying or doing slow work. Edge Functions (netlify/edge-functions/*) run on Deno Deploy at every edge location, can transform any response in flight, and are the right layer for content negotiation. The audit makes an HTTP request and judges the response — Edge Functions are the layer that gets to alter that response without a measurable latency penalty.
You don't need both. Pick Edge Functions and do the whole negotiation there.
Pattern A — Companion file (simplest)
If your site is built from markdown source (Eleventy, Hugo, Astro, Next.js with MDX, Gatsby) you already have the markdown sitting on disk. Publish a .md companion at every URL you care about and let the Edge Function serve it on negotiation.
Step 1 — Publish the companion files. In Eleventy, add a passthrough copy for your .md source:
eleventyConfig.addPassthroughCopy({ "src/blog": "blog" });
Now /blog/foo/index.md is reachable at /blog/foo/index.md. In Hugo, set outputs = ["HTML", "Markdown"] in your config; in Astro, set output: 'static' and add a .md route handler.
Step 2 — Create the Edge Function. Create netlify/edge-functions/markdown.ts:
import type { Context } from "@netlify/edge-functions";
export default async (request: Request, context: Context) => {
const accept = request.headers.get("accept") || "";
if (!/text\/markdown/i.test(accept)) return;
const url = new URL(request.url);
let mdPath = url.pathname;
if (mdPath.endsWith("/")) mdPath += "index.md";
else if (!mdPath.endsWith(".md")) mdPath += ".md";
const mdUrl = new URL(mdPath, url.origin);
const mdResp = await fetch(mdUrl);
if (!mdResp.ok) return; // fall through to HTML
const body = await mdResp.text();
return new Response(body, {
status: 200,
headers: {
"content-type": "text/markdown; charset=utf-8",
"vary": "Accept",
"cache-control": "public, max-age=300",
},
});
};
export const config = { path: "/*" };
Step 3 — Wire it in netlify.toml (only needed if you want to scope the path differently than config.path):
[[edge_functions]]
function = "markdown"
path = "/*"
Deploy. The function returns nothing on a request without Accept: text/markdown — Netlify's response chain continues to the static HTML. On a markdown request, it serves the companion file with the correct Content-Type.
Pattern B — Runtime HTML-to-markdown conversion
If your source is not markdown (a CMS-driven site, a hand-coded HTML site, a SPA where the source is JSX), convert at the edge. Deno has solid HTML-to-markdown libraries; the lightest one for Edge Functions is turndown via the Skypack bridge, or the pure-Deno deno_dom + a small custom converter for full control.
import type { Context } from "@netlify/edge-functions";
import TurndownService from "https://esm.sh/turndown@7.2.0";
export default async (request: Request, context: Context) => {
const accept = request.headers.get("accept") || "";
if (!/text\/markdown/i.test(accept)) return;
const htmlResp = await fetch(request.url, {
headers: { accept: "text/html" },
});
if (!htmlResp.ok) return;
const html = await htmlResp.text();
// Trim to the main content shell. Adjust the selector to your site.
const mainMatch = html.match(/<main[^>]*>([\s\S]*?)<\/main>/i);
const target = mainMatch ? mainMatch[1] : html;
const td = new TurndownService({ headingStyle: "atx", codeBlockStyle: "fenced" });
const md = td.turndown(target);
return new Response(md, {
status: 200,
headers: {
"content-type": "text/markdown; charset=utf-8",
"vary": "Accept",
"cache-control": "public, max-age=300",
},
});
};
export const config = { path: "/*" };
The <main> extraction is the crucial step. Without it, your nav, footer, sidebar, and ad markup all end up in the markdown twin — which defeats the entire point. Edit the selector to match your template's main content container.
The Vary: Accept header is mandatory
Whichever pattern you pick, set Vary: Accept on the markdown response. Without it, Netlify's CDN may serve your markdown to a browser that asked for HTML, breaking the visible page. Netlify's edge cache respects Vary correctly when the header is present; absence of Vary collapses the two response shapes into one cache entry, and the wrong one will leak.
Verifying the fix
After deploying, run:
curl -s -H "Accept: text/markdown" -i https://yoursite.com/some-page/ | head -10
The response should show content-type: text/markdown; charset=utf-8 and a vary: Accept header in the first ten lines, with markdown-formatted body following. Re-run the Agent Runtime Readiness audit on the same URL and the third check should flip to a pass.
If the audit still warns:
- Edge Function isn't running. Check the Netlify deploy log; an Edge Function with a parse error fails silently in production. The deploy log lists every function it loaded.
- The
Varyheader is missing. Open your browser dev tools, request the URL with the markdown Accept header (curl is easier), and confirm the response includesvary: accept(case-insensitive in HTTP). - A redirect is intercepting first. Netlify's
_redirectsfile rules apply before Edge Functions on some path patterns. Check_redirectsfor any/*catch-all that could be returning before the function runs.
What this costs
Netlify Edge Functions are billed per invocation. The free tier includes 1 million invocations per month; current pricing past that is small (cents per million). For a site with modest AI-runtime traffic, this is well under a dollar per month even after you exceed free tier. The runtime-conversion pattern uses more CPU per call than the companion-file pattern; if your traffic is heavy and you have markdown source available, prefer Pattern A.
Related reading
- The Original Markdown For Agents Warning Post — what the audit warning means and the Cloudflare-toggle path
- Agent Runtime Readiness — the audit tool itself, all three checks
- The Conversation Has Moved Past The Model — why agent runtimes are the new browser layer
- Vercel Edge Middleware Pattern — same fix, different platform
- Origin Server Configs (Nginx / Apache / Caddy) — if you don't have an edge layer at all
Fact-check notes and sources
- Netlify Edge Functions reference: docs.netlify.com/edge-functions/overview/
- Deno Deploy runtime (what Netlify Edge Functions run on): deno.com/deploy/docs
- Vary header semantics that make Accept-based content negotiation safe to cache: RFC 9110 §12.5.5
- Turndown HTML-to-Markdown library: github.com/mixmark-io/turndown
- Cloudflare Markdown for Agents reference (the feature this post replicates on Netlify): developers.cloudflare.com/fundamentals/reference/markdown-for-agents/
If you're running Netlify as part of a leaner build-your-own-web stack — site, audit loop, mailing infrastructure, all owned by you — The $20 Dollar Agency covers that operating model end to end.
This post is informational, not legal or SEO-consulting advice. Mentions of Netlify, Cloudflare, Deno, and other third parties are nominative fair use; no affiliation is implied.