← Back to Blog

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

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

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 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

Fact-check notes and sources

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.

← Back to Blog

Accessibility Options

Text Size
High Contrast
Reduce Motion
Reading Guide
Link Highlighting
Accessibility Statement

J.A. Watte is committed to ensuring digital accessibility for people with disabilities. This site conforms to WCAG 2.1 and 2.2 Level AA guidelines.

Measures Taken

  • Semantic HTML with proper heading hierarchy
  • ARIA labels and roles for interactive components
  • Color contrast ratios meeting WCAG AA (4.5:1)
  • Full keyboard navigation support
  • Skip navigation link
  • Visible focus indicators (3:1 contrast)
  • 44px minimum touch/click targets
  • Dark/light theme with system preference detection
  • Responsive design for all devices
  • Reduced motion support (CSS + toggle)
  • Text size customization (14px–20px)
  • Print stylesheet

Feedback

Contact: jwatte.com/contact

Full Accessibility StatementPrivacy Policy

Last updated: April 2026