Skip to content

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.

An agent fetching /recipes/oatmeal.md: the response carries Content-Usage, Content-Signal, a license Link, an ETag, Last-Modified and token estimate headers, then YAML front matter with the usage terms, then the recipe as Markdown.

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 getting 304 Not Modified after 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.

The Agent Surface requests report: tables by user agent, by day and by selection rule, each with requests, 304s, the 304 ratio, bytes served, tokens served and tokens saved.