---
title: "Raw markdown API"
description: "Every documentation page is available as markdown at the same URL with .md appended. You get processed markdown, not MDX source, with components left as tags."
url: "https://opsinjs.pensievelabs.org/agents/raw-markdown-api"
source: "https://opsinjs.pensievelabs.org/agents/raw-markdown-api.md"
section: "Agents & automation"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: [".md", "markdown twin", "copy page", "page as markdown"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<PageTemplate kind="guide" />

## Overview [#overview]

Append `.md` to any documentation URL and you get that page as markdown.

```bash
curl -s https://opsinjs.pensievelabs.org/health/two-colour-axes.md
```

That is the whole API. There is no key, no endpoint, no pagination and no
separate documentation site for machines. The twin is the same page, and it
cannot fall out of date with the human version because it is produced from the
same source at the same time.

## Processed markdown, not source [#processed-markdown-not-source]

This is the part that matters and the part most sites get wrong.

The twin is not the raw `.mdx` file. Raw MDX carries imports, and its JSX is
written for this application rather than for a reader; a model handed the source
either ignores half of it or invents what it contained.

The twin is *processed*: remark has run over the MDX and stringified it back to
markdown, so imports are gone, headings carry explicit ids, and tables and code
blocks arrive as markdown. What processing does **not** do is render the
documentation components. `<StubNotice name="range-bar" status="shipped" …/>`
comes out verbatim, attributes and all, and the sentence it renders in the
browser is not in the string. That is measured rather than assumed, because it
is written down in `app/_machine/corpus.ts`. This surface is built around it
instead of pretending otherwise.

Three consequences worth knowing:

* **The tags are the machine-readable part.** A component's attributes are its
  content: `<ComponentPreview name="range-bar" />` names the component,
  `<ContrastReport scope="materials" />` names the scope. The values those
  components draw are generated from `tokens/*.json` and `registry/catalogue.ts`
  and published separately, at `/r/index.json` and under the Reference section.
  Nothing on the page is unreachable; it is just not in the prose.
* **A twin that contains tags says so.** Any twin with a `<PascalCase … />` in
  it carries a block quote, between the frontmatter and the body, explaining
  what the elements are and where their data lives. A page that is not
  implemented carries a second quote above that one saying so. If you are
  writing a parser, those quotes are the cue to go and fetch the data rather
  than errors to report.
* **Frontmatter is rebuilt, not copied through.** The twin carries the authored
  fields plus `url`, `source` and `section`, so a page read in isolation still
  knows where it came from. On a component page `status` is the field to trust,
  and it reads `shipped` for a built component. One field is not
  authored at all: a component page or a screen specimen documents something
  buildable, and its twin also carries `implemented`, read from the generated
  index rather than from the MDX, so no edit to the page can contradict it. A
  guide, a doctrine page or a generated token reference carries no
  `implemented` key, because it documents nothing that could be built and a
  `false` there would assert something untrue about a page that is real today.
  For a per-id answer outside the docs tree, read `implemented` on
  `/r/index.json`, which is where that field is generated and where it is
  guaranteed current.

## The docs version header [#the-docs-version-header]

Every twin response carries the docs version on the `x-opsinjs-docs-version`
header. It is a header and not a frontmatter key, which matters more than it
sounds: a twin saved to a file loses it, so read it at fetch time and store it
beside the body yourself. Without it a cached copy cannot be distinguished from a
current one, and a bug report six months from now cannot say *which* version of
the documentation an agent read.

Four more headers ride on the same response, and the five of them do not all
answer at the same scope. Mistaking one for another is the expensive error, so
the scope is worth reading off a table rather than inferring:

| Header                        | Answers about                                                                    | Value                                                                |
| ----------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| `x-opsinjs-docs-version`      | The site                                                                         | The docs version this build serves                                   |
| `x-opsinjs-kind`              | This page                                                                        | The page's own `kind`, sent on every twin                            |
| `x-opsinjs-status`            | This page, on a `kind: component` page only                                      | That component's release phase: `planned`, `shipped` or `deprecated` |
| `x-opsinjs-implemented`       | This page, when the page documents a component or a screen; otherwise the system | Whether that subject has source behind it                            |
| `x-opsinjs-implemented-count` | The system                                                                       | How many component ids have real source behind them                  |

A component or screen twin overrides `x-opsinjs-implemented` with its own
subject's answer, exactly as `/r/<id>.json` does, so `HEAD` on a component's
`.md` reports that one component's source status rather than the system's. A
A `planned` id would return `false` there even though the count beside it is not
zero, and every catalogue id is built today so every component twin
returns `true`. Every other page names nothing buildable, whether
it is a guide, a handbook chapter or a doctrine page, so the header keeps its
system value there, which answers "is anything implemented" rather than "is
this implemented". `x-opsinjs-kind` is sent on every twin, and it is how you
tell which of the two scopes you were handed: a twin with no `x-opsinjs-kind`
at all is being served by a build older than this contract, and its
`x-opsinjs-implemented` is the system answer whatever the page is about.
`x-opsinjs-status` is a narrower signal and is absent on most twins by design.
A release phase belongs to a component, so only a `kind: component` page sends
one; its absence means the page is not a component, never that the page is
unfinished. If
you need a per-id answer for something that has no documentation page, ask
`/r/<id>.json` or the roster at `/r/index.json` instead of inferring one from a
header.

There is no per-page last-modified date, in the frontmatter or anywhere else.
Nothing in the build computes one, and a fabricated date would be worse than an
absent one. The version is the only freshness signal this surface offers.

If you cache twins, cache the version with them, and include it when you report
a problem.

## What has a twin and what does not [#what-has-a-twin-and-what-does-not]

Every page under the documentation tree has one. So does the documentation index
itself, at `/index.md`.

Nothing else does. The playgrounds, the colour browser and the
preview host are applications, not documents, and there is no meaningful
markdown rendering of an interactive tool. Asking for their twin returns the
application.

## Verify it worked [#verify-it-worked]

<Steps>
  ### You get markdown, not HTML [#you-get-markdown-not-html]

  The first bytes should be a frontmatter delimiter, not `<!DOCTYPE`.

  ### PascalCase tags are present and explained [#pascalcase-tags-are-present-and-explained]

  Search the output for a `<` followed by an uppercase letter. On most pages you
  will find several, and that is the twin working: read their attributes, and take
  the block quote above them as the pointer to where the rendered values are
  published. What you should *not* find is an `import` statement or a `<!DOCTYPE`.
  Either one means you have raw source or the HTML page rather than the twin.

  ### The status is present [#the-status-is-present]

  Every component page's twin carries `status` in frontmatter, and the value is
  `shipped` for a built component. If
  a component twin omits `status` altogether, that is a bug worth reporting: it is
  the field that stops an agent generating against something that does not exist. A
  twin for any other kind of page carries no `status`, which is correct rather than
  a gap.
</Steps>

## Troubleshooting [#troubleshooting]

**404 on a `.md` URL.** The path is not a documentation page. Twins exist for
the docs tree and `/index.md` only.

**The twin shows a tag where the page shows a table.** Expected. The
documentation components are not rendered into markdown; fetch the values from
`/r/index.json` or the Reference section. If the *human* page shows the same
gap, that is a different fault. An empty table there rather than a filled one
means `pnpm run generate` had not run when the page was built.

**The prose differs between the twin and the page.** The rendering differs by
design; the words should not. Report a wording difference. That invariant is
what the whole surface rests on.

**I need many pages at once.** Do not crawl. Use
[the shards](./llms-txt.mdx) or [the offline bundle](./offline-docs-bundle.mdx),
both of which are one request.

## Next [#next]

* [llms.txt and the shards](./llms-txt.mdx) shows how to find the pages worth
  fetching.
* [Offline docs bundle](./offline-docs-bundle.mdx) has every twin in one file.
* [Machine-readable schemas](./machine-readable-schemas.mdx) covers the
  structured data behind the prose.
