Usage¶
How a request gets Markdown¶
The rules are checked in order, and the first match wins.
| Trigger | Result | Shared caches |
|---|---|---|
Path ends in .md, or starts with /markdownify/ |
Markdown | Cacheable: the URL carries the format |
?_format=markdown or ?_wrapper_format=markdown |
Markdown | Cacheable: the query string is part of the key |
Accept ranks text/markdown above text/html |
Markdown | private, no-store |
User-Agent matches the configured list |
Markdown | private, no-store |
| Otherwise | HTML | Cacheable |
Accept: */* never means Markdown: curl, most crawlers and many HTTP
libraries send it and want a web page.
The first three rules are Markdownify's own. Agent Surface names them so it
can tell shareable Markdown from Markdown that must stay out of shared caches,
and adds the fourth. A Markdown body served at a URL that says HTML never
enters a cache keyed on the URL alone, because the next visitor to that URL
could be a browser. No Vary: User-Agent is ever sent: on a shared CDN its
cardinality is effectively unbounded.
Selection runs in a middleware ahead of Drupal's internal page cache, so a cached HTML page is never handed to a matched agent, and a negotiated request is never stored. A matched agent on a route that cannot be rendered as Markdown gets the HTML page instead of a 406.

Behind a CDN¶
The .md URL is the reliable path because it needs no Vary at all. A CDN
keyed on the URL serves its cached HTML to an Accept: text/markdown request
and to a matched user agent alike, so header negotiation only takes effect on
cache misses. That is acceptable as long as the Markdown responses never
enter the cache, which private, no-store guarantees.
Conditional requests¶
On shareable Markdown responses the module sets:
ETag, a strong validator derived from the body. A cache rebuild that produces the same Markdown produces the same ETag, so crawlers keep getting304 Not Modifiedafter a deploy. Core's own ETag is the response timestamp, which changes on every rebuild.Last-Modified, the entity's changed time where the route shows an entity.
If-None-Match and If-Modified-Since are answered with an empty 304, both
on fresh renders and on page cache hits; the page cache keeps the full 200.
Two estimate headers, X-Markdown-Tokens and X-Original-Tokens (about four
characters per token), let an agent decide whether to fetch the body.
Front matter¶
Static files carry no headers, and a body pasted into a context window keeps its front matter but loses its headers. So the declarations also travel as YAML front matter. Agent Surface writes:
| Key | Value |
|---|---|
language |
The entity's language, or the current content language. |
alternates |
The .md URLs of the entity's other translations the visitor can view, keyed by language. |
content-usage, content-signal |
One per enabled vocabulary. |
license |
The licence URL, with the RSL submodule. |
When Markdownify Metadata (or any other module using
hook_markdownify_entity_markdown_alter()) has already written a block, these
keys are merged into it; Agent Surface's implementation of that hook runs
last. Without such a block, a smaller one with title, canonical,
published, updated and tags is written.
HTML pages¶
Every HTML page that has a Markdown twin advertises it with
<link rel="alternate" type="text/markdown"> and the matching HTTP Link
header, and carries the signal headers (Content-Usage, Content-Signal, and
Link: rel="license" with RSL). rel="canonical" stays with Metatag: one
canonical HTML URL with a marked alternate keeps the Markdown from reading as
duplicate content.
Access¶
A .md URL answers exactly when its HTML URL does: same access checks, same
node grants, same handling of unpublished content. Fields hidden by the view
mode stay hidden, because the Markdown is converted from the rendered output.
Administrative routes never convert, whoever asks.
Request log¶
Every Markdown request and every matched-agent request is recorded after the
response is sent, including requests the internal page cache answers and the
304s given for them. Each row holds the time, path, language, user agent
(truncated), the Signature-Agent header when present, the selection rule,
status, bytes, estimated tokens and the cache layer that answered. No IP
addresses, cookies or session identifiers are stored. A 304 counts no bytes
and no tokens.
The report at Reports → Agent Surface requests
(/admin/reports/agent-surface) aggregates by user agent, by day and by
selection rule. The 304 ratio shows how many repeat fetches were answered
without a body.
