Cross-references & the shape of the site
The documentation is not a set of pages. It is one hypertext with six views over it — the docs, the glossary, the component catalog, the toolbelt, the API object model and the live showcase — and every one of those views is generated from the same files in docs/ and src/.
That is the whole advantage, and it is also the whole risk. Six generated views means six independent implementations of "what does this link to", and the failure mode is never a broken build. It is a link that lands on the right page at the wrong place, or a page that nothing points at.
The one rule
Every surface links outward the same way, through the same component.
The alternative — each page inventing its own related-links block, in its own shape — is how a documentation site ends up with pages nothing points at. When there is one component, an orphan is a missing entry rather than a missing design, and it shows up in a diff.
The components
SeeAlso
The standard "where to go from here" block. It takes a list of typed cross-references and renders them as a ruled list with the kind stated on each row.
<SeeAlso
description="A component is specified in one place, defined in another, and running in a third."
links={[
{ kind: "doc", href: component.docUrl, title: "Payables — the family spec" },
{ kind: "showcase", href: "/showcase/ap", title: "The family, running" },
{ kind: "term", href: "/glossary/three-way-match", title: "Three-way match" },
]}
/>
The kind is not decoration. The six surfaces are genuinely different kinds of answer:
| Kind | What the reader gets |
|---|---|
term |
A word, defined in one sentence |
doc |
An argument for why something is the way it is |
component |
Code you install and then own |
tool |
A number, computed |
api |
The shape of a payload |
showcase |
The thing running, with figures in it |
A reader who cannot tell which of those a link leads to has to click to find out, and mostly does not click.
TermRef and automatic glossary linking
A reader who hits three-way match in the middle of a payables spec has no way to know it is defined elsewhere, and the block of terms at the foot of the page is only useful to someone who already finished reading. So the renderer links the first mention of each glossary term in a document's prose.
Restraint is the entire design:
- First mention only, and never a term the document is itself the definition of.
- Never inside a heading, a link, a code span or a code block.
- Never a term that is also ordinary English — account, period, balance, statement. Those concepts are still in the glossary; they are just not worth interrupting a sentence for.
- Never more than twelve on one page. Past that, the page is a glossary.
Term links are dotted and in the text colour, not the accent. An author's link is a destination they chose; an auto-linked term is a definition the build offered. The two should not look the same.
PageHeader aside
The one place on a page where an illustration is allowed to sit — beside the header, dropped entirely on narrow viewports. See imagery & isometric.
Anchors: one algorithm, two consumers
Every document in docs/ is read in two places — on this site and on GitHub — and an author writing #law-2--data-does-not-re-animate is reading that anchor off the GitHub heading.
So headingId in src/lib/markdown.ts is GitHub's slug algorithm, deliberately, and scripts/build-glossary.mjs mirrors it when recording the anchor a term is defined at.
The subtle part: GitHub maps each space to a dash rather than collapsing runs of whitespace. A heading containing an em dash, a slash or an ampersand loses that character and leaves two spaces behind — so Law 2 — Data does not re-animate is law-2--data-does-not-re-animate, with two dashes. Collapse the whitespace instead and every such anchor silently lands at the top of the page rather than at the section.
The two implementations live in different languages in different directories. tests/doc-links.test.ts is the assertion that they agree.
Section indexes
Seven of them, one per directory under docs/, at /docs/<section>.
They exist for three reasons at once:
- The source markdown links to directories —
[the domain layer](domain/), written just like that — because on GitHub a directory is exactly what that is. Those links have to land somewhere on the site. - A reader who arrives mid-sequence from a search result needs the shape of the section they landed in.
- Before them, every section had exactly one inbound link: the sidebar. A sidebar is a list, not a structure.
SECTIONS in src/lib/docs-sections.ts is the table. It is data rather than markup so the link tests can import it without pulling JSX through the test transform — and it must stay in step with the directories on disk, because a document in a directory no section names falls back to "Start here" and sits next to the manifesto. That is not a graceful degradation; it is a filing error that nobody sees. The test asserts the two match.
What the tests hold
tests/doc-links.test.ts is the only thing standing between this structure and slow rot:
- Every relative link resolves to a document or a section index.
- Every anchor resolves to a heading the site actually renders — checked against the rendered HTML, not against a re-implementation of the rule.
- Every glossary term points at a heading that exists.
- Every component points at a family document that exists.
- Every directory under
docs/has a navigation section. - No auto-linked term is ever nested inside another link, a heading or a code block.
None of these can be caught by tsc, and none of them break the build. They break the reader.