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

If your site is served directly from Nginx, Apache, or Caddy with no CDN edge layer in front, you can still implement the Markdown for Agents content-negotiation pattern. Three side-by-side configs — what to add, where to add it, and how to test it works.

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

---

If you ran a URL through the [Agent Runtime Readiness](/tools/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:

```nginx
# 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`:

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

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

```bash
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](/tools/agent-runtime-readiness/) 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:

```php
// 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

- [The Original Markdown For Agents Warning Post](/blog/blog-fix-markdown-for-agents-warning/) — what the audit is checking and the Cloudflare-toggle path
- [Agent Runtime Readiness](/blog/blog-tool-agent-runtime-readiness/) — the audit tool itself
- [The Conversation Has Moved Past The Model](/blog/blog-agent-runtime-the-new-browser-layer/) — why this matters now
- [Netlify Edge Functions Pattern](/blog/blog-fix-markdown-for-agents-netlify/) — same fix on Netlify
- [Vercel Edge Middleware Pattern](/blog/blog-fix-markdown-for-agents-vercel/) — same fix on Vercel
- [Squarespace and GoDaddy](/blog/blog-fix-markdown-for-agents-squarespace-godaddy/) — what's possible on closed and managed-WordPress hosts
- [Caddy Server Use Cases](/blog/blog-caddy-server-use-cases/) — broader context on running Caddy as your origin

## Fact-check notes and sources

- Nginx `map` directive reference: [nginx.org/en/docs/http/ngx_http_map_module.html](https://nginx.org/en/docs/http/ngx_http_map_module.html)
- Apache `mod_rewrite` documentation: [httpd.apache.org/docs/2.4/rewrite/](https://httpd.apache.org/docs/2.4/rewrite/)
- Caddy `header_regexp` matcher: [caddyserver.com/docs/caddyfile/matchers#header-regexp](https://caddyserver.com/docs/caddyfile/matchers#header-regexp)
- Vary header semantics for Accept-based negotiation: [RFC 9110 §12.5.5](https://www.rfc-editor.org/rfc/rfc9110#field.vary)
- Cloudflare Markdown for Agents reference (the feature this post replicates origin-side): [developers.cloudflare.com/fundamentals/reference/markdown-for-agents/](https://developers.cloudflare.com/fundamentals/reference/markdown-for-agents/)

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


---

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