opsinjs
HandbookContributing

Documentation templates

Twelve page kinds, each with a fixed set of headings the build enforces. The outlines are published verbatim, so a contributor fills in a template.

The short version

A page's kind fully determines its headings. Authors do not invent sections, do not reorder them, and do not leave one empty. assert-ia compares the H2s in every page against the outline for its kind and fails the build on a missing or an unexpected one.

That sounds heavy-handed and it is the reason four hundred pages stay navigable. A reader who has read one health page knows where the boundary statement is on every other one; an agent that has parsed one component page knows where the prohibition list lives on all sixty.

The templates themselves live in apps/www/content/_templates/. There is one .mdx per kind, kept outside content/docs/ so they are never routed. Copy one; do not start from a blank file.

How it works

Twelve kinds. The outlines are reproduced here verbatim, in order. Where a section is generated, it is marked.

component

The lean anatomy, taught once on Anatomy of a component page. <StubNotice> opens the page above the first H2, and there is no Status heading. The sections that render are gated by status: at planned the page shows Preview, Installation, When to use it, Clinical meaning (health-* only), Anatomy (optional), Proposed API, Accessibility and Related. shipped adds Usage, Examples and API reference, and may also carry States, Content guidelines, Data attributes and CSS variables where there is something to say. deprecated owes the same outline as shipped. Sections not required at the declared status are omitted, not left empty, and the body is held to a thousand words, or twelve hundred and fifty on a health-* page.

foundation

## Overview## How it works## Using it## Tokens (generated)## Accessibility impact## Related

health

## What this means## The rule## Why (evidence)## Applying it## Components that implement this (generated from implements)## What this does not cover## Updates to this page

evidence and reviewed are mandatory frontmatter on this kind.

accessibility

## What we guarantee## What you own## How to check## Measured results (generated)## Known gaps## Updates to this page

content

## The rule## Approved / Rejected## Patterns## Banned words## Related components

pattern

## When to use## When not to use## How it works## Content## Accessibility## Research## Updates to this page

Every entry under "when not to use" names the alternative.

recipe

## The task## What you need## Build it## The copy## Get it right## Variations## Related

screen

## What this screen does## Composition## Preview## Safety notes## Accessibility## Status

handbook

## The short version## How it works## Do this## Not this## Gotchas## Related

This page is one.

reference

Generated body only, plus a hand-written header block naming the source file and the script that produces it.

project

Free-form headings, but every page ends with <LastUpdated /> and <Reviewed />.

guide

## Overview → one H2 per task, named by the author → ## Verify it worked## Troubleshooting## Next

The four fixed sections and their order are the contract; the task sections in between are yours.

Do this

  • Copy the template file for your kind. content/_templates/<kind>.mdx.
  • Fill every section. On every kind but component, a section with one sentence under it is a signal that either the page or the kind is wrong. On a component page one sentence is often the whole answer.
  • Use <Todo> for a genuine gap. It is counted in the build's coverage report, which is the point: incompleteness is measured rather than hidden.
  • Use the closed MDX vocabulary. The tags listed in the component anatomy are the only ones that exist; assert-ia fails the build on an unknown JSX tag, and content authors never define one.
  • Write relative links with the .mdx extension. [Two colour axes](../../health/two-colour-axes.mdx). fumadocs' createRelativeLink resolves them.
  • Write a related-links bullet as a sentence. The linked page or the bold term is the subject, and the verb comes from a closed table. A page that is the thing its gloss names takes is, a page that holds the thing takes has, and where neither reads true the verb is covers. A gloss opening with what, which, whether or when takes says. One opening with where takes is where, which absorbs the where out of the gloss. why takes explains, how takes shows, and any other bare noun phrase takes covers. A gloss that already opens with a finite verb or a modal takes no added verb at all. Where the gloss is not a predicate of the link, give the link a short predicate of its own and put the gloss into a second sentence. The link text stays the page title, the gloss keeps its wording and its lower-case first letter, the item ends in a full stop, and no colon stands between the link and its gloss.

Not this

  • Do not add a heading the outline does not have, however good it is. If a kind genuinely needs a new section, change the kind's outline in one place and every page that uses it.
  • Do not leave a required section empty to satisfy the check. An empty section is a worse answer than <Todo>, because <Todo> is counted.
  • Do not use an absolute site link in MDX. Write a relative file link and let fumadocs resolve it. There is no exception left: the one that used to exist, the Sections rail in the root meta.json, was deleted with the rail.
  • Do not restate a canonical page. Plain-English A to Z is canonical for plain language; Numbers, units and precision is canonical for numeric formatting. Link, do not repeat. assert-ia checks for restatement.
  • Do not hand-write a generated section. Tokens, contrast numbers, keyboard tables, prop tables and the catalogue are produced by pnpm run generate.

Gotchas

  • The H2 check is exact. "Not this" and "What not to do" are not the same heading, and the build says so.
  • kind is not a category. It selects an outline. A page about health that is really a step-by-step is kind: guide living under start/, not kind: health with the wrong headings.
  • Status gating applies only to kind: component. Every other kind renders its full outline at every status.
  • The templates are outside content/docs/ on purpose. Moving one inside creates a route and a page with placeholder prose in the search index.
  • The frontmatter schema and source.config.ts must agree. content/_templates/frontmatter.schema.json is the machine-readable form; source.config.ts is what the build actually enforces.

On this page