A markdown alternate is a low-tech answer to a real problem: a page built for people is full of navigation, scripts and layout, and an engine that wants the text has to strip all of it. Publishing a plain-text representation and pointing at it with a link element is cheap. The hard part is not the representation; it is keeping it true, scoped to the pages that have one, and not accidentally creating a second indexable copy of the site.
This article is about the declaration, the twin behind it, and the rules that keep the two from drifting.
What the declaration looks like
The convention is a link in the document head:
<link rel="alternate" type="text/markdown" href="/page/markdown/" />
The scanner’s rule is markdown-alternate, worth 1 point: the head contains a rel="alternate" link with type="text/markdown", in either attribute order. It reads the declaration only and does not follow it, which the rule page states: a page can pass while pointing at a link that 404s.
One point is the right weight. The declaration is cheap, the convention is not standardised, and no engine has committed to reading it. A site that publishes twins because they are useful is making a different decision from a site that adds the link to satisfy a check.
Two ways to serve the representation
| Approach | How it works | Cost |
|---|---|---|
| Separate URL | A route such as /page/markdown/ returns text/markdown; the HTML page links to it. | Cacheable, linkable, easy to test; two URLs to keep in sync. |
| Content negotiation | The same URL returns HTML or markdown depending on the Accept header. | One URL; needs Vary: Accept and cache discipline, and clients that do not send the header get HTML. |
This site uses the separate URL. The homepage twin is served by a route, the rule pages publish theirs from their own handlers, and the responses carry a x-markdown-twin-of header naming the HTML page they represent. The header is not required; it is a debugging aid that makes the relationship visible in a response without parsing the body.
The separate URL is also why the declaration is scoped. The site’s homepage declares the twin in the homepage layout, not the root layout, because an inherited declaration would put a link to /markdown/ in the head of every page and tell a machine that a representation of somebody else’s page lives there. A declaration should be as narrow as the set of pages that actually have a twin.
The twin has to come from the same source
The failure that matters is drift. If the markdown is written by hand next to the HTML, the two disagree within a release: a price changes, a claim is corrected, a section is removed, and the twin keeps serving the old version. An engine that reads the twin then quotes something the page no longer says.
The fix is to generate both from the same data. On this site the twins are rendered from the same modules the pages use, which is why a check that compares the two is meaningful rather than decorative. The general rule: the markdown should be a rendering of the content, not a second authoring of it.
Common mistakes
- Declaring a twin that does not exist. The scanner does not fetch it; a reader does. A 404 behind a perfect declaration is worse than no declaration.
- Wrong type or wrong rel. A link with
type="text/plain"orrel="alternate"missing is not the declaration; it is a link. - Declaring it on every page from a shared layout. The link then claims a twin for pages that do not have one. Scope it to the pages that do.
- Serving HTML at the markdown URL. A catch-all route returns the app shell with a 200; the content type and the body are wrong, and the declaration points at a page.
- Letting the twin compete in search. If it is indexable, it can be treated as a separate page. The HTML page is the canonical version; make the twin’s indexing signals agree with that decision.
- Losing the structure. A twin that flattens headings, tables and lists into a wall of text throws away the thing that made the HTML extractable.
- Requiring the chrome. A markdown representation that starts with the navigation and cookie banner is not a representation; it is the page with the tags stripped.
How to test a twin
# 1. The declaration, from the head curl -sS https://your-domain.com/page/ \ | grep -oE '<link[^>]+type="text/markdown"[^>]*>' # 2. The twin resolves, with the right content type curl -sS -D - -o /dev/null https://your-domain.com/page/markdown/ # 3. The twin starts with markdown, not HTML curl -sS https://your-domain.com/page/markdown/ | head -n 5 # 4. The two agree on the main heading and roughly on length curl -sS https://your-domain.com/page/ | grep -oE '<h1[^>]*>[^<]*' curl -sS https://your-domain.com/page/markdown/ | head -n 1
The fourth comparison is crude and useful: the H1 and the first heading should match, and the markdown should not be an order of magnitude shorter. A twin that lost half the page is the failure mode; a twin that is slightly shorter because it dropped the chrome is the point.
What a markdown alternate buys
It buys efficiency for a reader that wants text: fewer tokens, no chrome, no parsing of layout. It also buys a testable representation — you can diff it, validate it, and check it in CI. It does not buy a ranking, a citation, or a guarantee that an engine reads it. It is a convenience offered to a machine, and the machine may decline.
The standard from the rest of this series applies: declare only what exists, generate it from the same source as the page, and make the relationship checkable. A twin nobody can verify is just a second copy waiting to disagree.