Skip to content

Configuration

mhupfauer edited this page May 31, 2026 · 1 revision

Configuration

The directive is markdown_for_agents. It accepts an optional inline argument (a shortcut for root) and an optional block.

Always use the block form for root. A bare markdown_for_agents /var/www/site is parsed by Caddy as a path matcher (/var/www/site), not a positional. See FAQ.

Minimal

example.com {
    root * /var/www/site
    markdown_for_agents {
        root /var/www/site
    }
    file_server
}

All directives

markdown_for_agents {
    root                /var/www/site
    cache_dir           /var/cache/md4agents
    url_suffix          .md
    query_param         format
    cache_size          8192
    cache_bytes         268435456
    cache_entry_bytes   1048576
    cache_ttl           24h
    max_body_bytes      8388608
    convert_timeout     5s
    max_concurrent      16
    pregenerate
    janitor_interval    1h
    allow_authenticated
    main_selector       article
    strip_tags          script style noscript nav footer aside
    strip_selectors     .ads "#cookie-banner"
}

Reference

Where content comes from

Directive Default What it does
root Static file root. Setting this enables the static-first path: requests resolve to disk before falling through to the next handler. Omit it to force the dynamic capture-and-convert path (for reverse_proxy, templates, etc.).
cache_dir <caddy-data-dir>/md4agents/<hash> Where generated *.html.md sidecars are written. Lives outside root; safe to delete at any time. Persist this across restarts for warm cache.

Content negotiation

Directive Default What it does
url_suffix .md URL suffix that requests Markdown (e.g. /page.md). Set to "" to disable. Stripped before the upstream sees the request.
query_param format Query parameter checked for the value md or markdown. Set to "" to disable. Stripped before the upstream sees the request.

The Accept header is always honored — Accept: text/markdown (with proper q-value handling vs text/html) triggers conversion regardless of the above two settings.

Caching

Directive Default What it does
cache_size 4096 In-memory LRU entry count.
cache_bytes 268435456 (256 MiB) Total in-memory cache byte budget. The cache is bounded by entry count and total bytes — whichever fills first triggers eviction.
cache_entry_bytes 1048576 (1 MiB) Per-entry size cap. Oversized responses are rejected outright (converted-and-served, but not cached).
cache_ttl 15m TTL for in-memory entries. Use 0 or never to disable TTL eviction entirely (operator must purge manually). Disk sidecars are invalidated via mtime, not TTL.
max_body_bytes 4194304 (4 MiB) Source HTML size cap (both static disk reads and dynamic captures). Larger responses pass through unconverted.

Conversion

Directive Default What it does
convert_timeout 5s Per-conversion timeout. On exceed, returns 503.
max_concurrent max(4, NumCPU) Semaphore bounding concurrent conversions (CPU/goroutine ceiling). Excess requests queue.
main_selector If set, only this element's subtree is converted. E.g. main_selector article extracts <article>...</article>.
strip_tags script style noscript iframe svg Tags removed entirely from output before conversion.
strip_selectors Simple tag, .class, #id selectors removed pre-conversion. Quote #id selectors# is a Caddyfile comment character otherwise.

Operational

Directive Default What it does
pregenerate false On startup, walk root and warm the cache for every HTML file. Useful for cold-start latency at the cost of provisioning time and RAM.
janitor_interval 0 (off) Periodic sweep that removes orphaned *.html.md sidecars (i.e., ones whose source HTML no longer exists).
allow_authenticated false If true, cache responses for requests carrying Authorization or Cookie headers. Only enable when upstream content is not user-specific — otherwise you'll serve User A's page to User B.

Defaults that surprise people

  • No TTL by default? No — there is a 15-minute TTL. Disk sidecars rely on source-file mtime, which is what most operators actually want.
  • Authenticated requests bypass cache by default. This is the conservative-correct choice; flip allow_authenticated only on public content.
  • Upstream Set-Cookie, Cache-Control: private, no-store, or a non-trivial Vary make the response uncacheable even if the request was. The module honors upstream caching hints.
  • The cache key for the dynamic path is path + ?query, so /api/p?id=1 and /api/p?id=2 don't collide.

Response headers

Every Markdown response includes:

Content-Type: text/markdown; charset=utf-8
ETag: "<sha256[:16]>"
Vary: Accept

If-None-Match is honored → 304 round-trip. HEAD requests return all headers (incl. Content-Length) and no body, per RFC 9110 §15.3.

Upstream headers from the dynamic path are whitelist-forwarded: Cache-Control, Expires, Last-Modified, Content-Language, Content-Security-Policy, Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy. Everything else — notably Set-Cookie, Server, X-Powered-By — is dropped.

See also

Clone this wiki locally