← Back to Blog

Fix The Markdown For Agents Warning Without A CDN — Nginx, Apache, And Caddy Configs

Fix The Markdown For Agents Warning Without A CDN — Nginx, Apache, And Caddy Configs

If you ran a URL through the Agent Runtime Readiness audit and the third check came back amber, you saw:

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 first half only applies if you're on Cloudflare. The second half — origin content negotiation — is what this post is about, for the increasingly common case of a site served directly from Nginx, Apache, or Caddy with no CDN sitting in front.

The pattern is the same on all three: detect the Accept: text/markdown request header, serve a .md companion file with the right Content-Type, set Vary: Accept so any downstream cache behaves correctly. The implementation differs by server.

Prerequisite — publish the companion .md files

This pattern assumes you have markdown source available alongside the rendered HTML. If your site is generated from markdown (Hugo, Eleventy, Jekyll, Astro), the source files are on disk already; copy them into the published output. If your site is template-based or CMS-driven, you'll need a step that generates the markdown twin at build time or on demand.

For a static-site generator, the easiest pattern is to have your build emit .md next to every index.html:

public/
  blog/
    foo/
      index.html
      index.md     ← companion file the negotiation rule serves

Once that's in place, the server config is a thin layer on top.

Nginx

Add this to your server block:

# Map Accept header to the file extension we should try first
map $http_accept $serve_md {
    default            "0";
    "~*text/markdown"  "1";
}

server {
    listen 443 ssl http2;
    server_name yoursite.com;

    root /var/www/yoursite/public;

    # Always advertise that responses vary by Accept
    add_header Vary "Accept" always;

    location / {
        # If client wants markdown and a .md companion exists, serve it
        if ($serve_md = "1") {
            rewrite ^/(.*)/$ /$1/index.md last;
            rewrite ^/([^.]+)$ /$1.md last;
        }
        try_files $uri $uri/ $uri/index.html =404;
    }

    # Make sure .md files get the right Content-Type
    location ~ \.md$ {
        default_type "text/markdown; charset=utf-8";
        add_header Vary "Accept" always;
        try_files $uri =404;
    }
}

Test the config (nginx -t) and reload (nginx -s reload).

The if directive in Nginx has a reputation, but in this narrow use (rewriting based on a mapped variable inside a location) it works correctly. The map block at the top is the cleaner way to test the Accept header — it's more cache-friendly than testing $http_accept directly inside if.

Apache

Apache has historically used mod_negotiation for content negotiation, but that's not the path here — mod_negotiation is built around MultiViews and type maps, which add complexity for a two-format use case. The simpler implementation is mod_rewrite:

<VirtualHost *:443>
    ServerName yoursite.com
    DocumentRoot /var/www/yoursite/public

    # Always set Vary: Accept on responses
    Header always set Vary "Accept"

    # Serve .md as text/markdown
    AddType "text/markdown; charset=utf-8" .md

    RewriteEngine On

    # If the client accepts text/markdown, rewrite the URL to .md companion
    RewriteCond %{HTTP_ACCEPT} text/markdown [NC]
    RewriteRule ^(.+)/$ /$1/index.md [L]

    RewriteCond %{HTTP_ACCEPT} text/markdown [NC]
    RewriteCond %{REQUEST_FILENAME} !\.md$
    RewriteCond %{REQUEST_FILENAME}.md -f
    RewriteRule ^(.+[^/])$ /$1.md [L]
</VirtualHost>

The two RewriteCond blocks check that the request URL doesn't already end in .md and that the corresponding .md file exists on disk before doing the rewrite. Without those checks, you can end up in rewrite loops or serving 404 pages with markdown Content-Type.

Reload Apache (apachectl graceful or systemctl reload apache2).

Caddy

Caddy's config language makes this the shortest of the three:

yoursite.com {
    root * /var/www/yoursite/public

    header Vary Accept

    @markdownAccept header_regexp Accept "text/markdown"

    handle @markdownAccept {
        # Try .md companion based on URL shape
        rewrite / {path}index.md
        rewrite * {path}.md
        try_files {path} {path}/index.md
        header Content-Type "text/markdown; charset=utf-8"
        file_server
    }

    handle {
        try_files {path} {path}/index.html
        file_server
    }
}

Reload Caddy (caddy reload --config /etc/caddy/Caddyfile).

Caddy's header_regexp matcher is the cleanest header-pattern matching of the three servers — and Caddy auto-handles HTTPS and HTTP/2, so the surrounding boilerplate is minimal.

The Vary: Accept header is mandatory

Whichever server you're on, Vary: Accept has to be on every response — both the HTML and the markdown one. If you have a downstream cache (a reverse proxy in front, a browser cache, an application-level cache), missing Vary: Accept causes the same problem you'd have on a CDN: the wrong response shape gets served to the wrong client.

The configs above set the header globally, which is the safe default. If you want it only on responses from this content-negotiation pattern, scope it to the rewrite block.

Verifying the fix

curl -s -H "Accept: text/markdown" -i https://yoursite.com/some-page/ | head -10

Look for content-type: text/markdown; charset=utf-8 and vary: Accept in the response. Re-run the Agent Runtime Readiness audit on the same URL — the third check should pass.

If the audit still warns:

  • The .md companion doesn't exist on disk. The rewrite is firing, the request is hitting a 404, and the 404 page may be HTML — which the audit will see as "not markdown." Confirm the file exists at the path the rewrite is targeting.
  • The rewrite isn't firing. Check the server's access log. If the URL in the log still ends in / or .html for a markdown-Accept request, the rewrite condition didn't match. The most common cause is case-sensitivity in the Accept-header pattern.
  • Reverse proxy in front. If your origin is behind a reverse proxy, an upstream proxy, or any application-level caching layer (Varnish, HAProxy, the WordPress object cache, etc.), the cache may be collapsing the two response shapes. Verify Vary: Accept is present on the origin response, then check what each layer in the chain does with it.

What about WordPress, Drupal, Rails, etc.?

If your origin is a CMS or app server rather than static files, the .md companion approach still works — you just need a route or filter that generates the markdown twin from the rendered post.

For WordPress, the simplest path is a must-use plugin that hooks template_redirect and outputs markdown when the request Accept header matches:

// wp-content/mu-plugins/markdown-for-agents.php
<?php
add_action('template_redirect', function () {
    if (!is_singular()) return;
    $accept = $_SERVER['HTTP_ACCEPT'] ?? '';
    if (stripos($accept, 'text/markdown') === false) return;

    global $post;
    $title = get_the_title($post);
    $content = strip_tags(apply_filters('the_content', $post->post_content));
    $md = "# {$title}\n\n{$content}\n";

    header('Content-Type: text/markdown; charset=utf-8');
    header('Vary: Accept');
    echo $md;
    exit;
});

This is a minimal example — strip_tags is not a real HTML-to-markdown converter, but it gets the basic case working. For higher fidelity, add the league/html-to-markdown Composer package and call its converter from the same hook.

Related reading

Fact-check notes and sources

If you're running your own origin server as part of a build-your-own-web stance — full control over the box, the audit loop, and the operating model — The $20 Dollar Agency covers that approach end to end.

This post is informational, not legal or SEO-consulting advice. Mentions of Nginx, Apache, Caddy, WordPress, 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