# Fix The Markdown For Agents Warning On Netlify — Edge Functions Pattern

If you saw &quot;Host did not return Markdown content&quot; from the Agent Runtime Readiness audit and your site is on Netlify, the fix is a 30-line Edge Function that intercepts Accept: text/markdown and serves a clean markdown twin. Two patterns — companion file and runtime conversion — with the netlify.toml route config.

Author: J.A. Watte
Published: May 9, 2026
Source: https://jwatte.com/blog/blog-fix-markdown-for-agents-netlify/

---

If you ran a URL through the [Agent Runtime Readiness](/tools/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](/blog/blog-fix-markdown-for-agents-warning/)). 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:

```js
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`:

```typescript
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`):

```toml
[[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.

```typescript
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:

```bash
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](/tools/agent-runtime-readiness/) 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 `Vary` header is missing.** Open your browser dev tools, request the URL with the markdown Accept header (curl is easier), and confirm the response includes `vary: accept` (case-insensitive in HTTP).
- **A redirect is intercepting first.** Netlify's `_redirects` file rules apply before Edge Functions on some path patterns. Check `_redirects` for 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](/blog/blog-fix-markdown-for-agents-warning/) — what the audit warning means and the Cloudflare-toggle path
- [Agent Runtime Readiness](/blog/blog-tool-agent-runtime-readiness/) — the audit tool itself, all three checks
- [The Conversation Has Moved Past The Model](/blog/blog-agent-runtime-the-new-browser-layer/) — why agent runtimes are the new browser layer
- [Vercel Edge Middleware Pattern](/blog/blog-fix-markdown-for-agents-vercel/) — same fix, different platform
- [Origin Server Configs (Nginx / Apache / Caddy)](/blog/blog-fix-markdown-for-agents-origin-servers/) — 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/](https://docs.netlify.com/edge-functions/overview/)
- Deno Deploy runtime (what Netlify Edge Functions run on): [deno.com/deploy/docs](https://deno.com/deploy/docs)
- Vary header semantics that make Accept-based content negotiation safe to cache: [RFC 9110 §12.5.5](https://www.rfc-editor.org/rfc/rfc9110#field.vary)
- Turndown HTML-to-Markdown library: [github.com/mixmark-io/turndown](https://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/](https://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.*


---

Canonical HTML: https://jwatte.com/blog/blog-fix-markdown-for-agents-netlify/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/blog-fix-markdown-for-agents-netlify.webp
