Cross-references & the shape of the site
The Lopifi site has six views (the docs, the glossary, the component catalog, the toolbelt, the API object model and the live showcase), and all six are generated from the same files in docs/ and src/.
That's the advantage and the risk. Six generated views means six separate answers to "what does this link to", and when one is wrong the build still passes. You get a link that lands on the right page at the wrong place, or a page nothing points at.
The one rule#
Every surface links outward the same way, through the same component.
Let each page invent its own related-links block and you end up with pages nothing points at. With one component, an orphan is a missing entry in a list, 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 tells the reader what they'll get. Each surface gives a different kind 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 |
If a reader can't tell which of those a link leads to, they have to click to find out, and mostly they don't.
TermRef and automatic glossary linking#
A reader who hits three-way match in the middle of a payables spec can't tell it's defined elsewhere, and a block of terms at the foot of the page only helps someone who has already finished reading. So the renderer links the first mention of each glossary term in a document's prose, with tight limits:
- First mention only, and never a term the document itself defines.
- Never inside a heading, a link, a code span or a code block.
- Never a term that is also ordinary English, like account, period, balance or statement. Those stay in the glossary but aren't worth interrupting a sentence for.
- Never more than twelve on one page. Past that, the page turns into a glossary.
Term links are dotted and in the text color instead of the accent. An author's link is a destination they chose. An auto-linked term is a definition the build offered, so it looks different.
PageHeader aside#
The one place on a page where an illustration may sit: beside the header, and dropped entirely on narrow viewports. See imagery & isometric.
Anchors: one algorithm, two consumers#
Every document in docs/ is read on this site and on GitHub, and an author writing #law-2--data-does-not-re-animate copied that anchor off the GitHub heading.
So headingId in src/lib/markdown.ts uses GitHub's slug algorithm on purpose, and scripts/build-glossary.mjs mirrors it when it records the anchor where a term is defined.
GitHub turns each space into a dash instead of collapsing runs of whitespace. A heading with an em dash, a slash or an ampersand loses that character and leaves two spaces behind, so Law 2 — Data does not re-animate becomes law-2--data-does-not-re-animate, with two dashes. Collapse the whitespace and every anchor like that lands at the top of the page instead of at the section.
The two implementations are in different languages in different directories, and tests/doc-links.test.ts checks that they agree.
The glossary and the founder explainers#
Many concepts have two pages: a definition at /glossary/<slug>, generated from the lexicon and the metric definitions, and a founder explainer at /accounting/<id> covering what the idea costs a startup and what to do about it in a given month. They answer different questions, so each keeps a self-referencing canonical.
They don't compete because each names the other through one field:
- An explainer's
links.glossaryis its definition./accounting/<id>renders it as the "Definition" link, and/glossary/<slug>lists every explainer whoselinks.glossaryis that slug under "For founders". - Where the two share a slug,
links.glossaryis that slug./accounting/lead-schedulepoints at/glossary/lead-schedule, never at a neighboring term. Point a shared slug anywhere else and the glossary page loses its founder link while the explainer shows the wrong definition.
If you add a lexicon term whose slug matches an explainer id, update that explainer's links.glossary in the same change.
Section indexes#
One per directory under docs/ — nine today — at /docs/<section>.
They exist for three reasons:
- The source markdown links to directories, written as
[the domain layer](domain/), because on GitHub that's what they are. Those links have to land somewhere on the site. - A reader who arrives mid-sequence from a search result needs to see the shape of the section they landed in.
- Before the indexes, the sidebar was every section's only inbound link, and a sidebar shows a list without the structure behind it.
SECTIONS in src/lib/docs-sections.ts is the table. It's data instead of markup so the link tests can import it without running JSX through the test transform. It has to match the directories on disk. A document in a directory no section names falls back to "Start here" and sits next to the manifesto, misfiled where nobody notices. The test checks the two match.
What the tests hold#
tests/doc-links.test.ts is the only thing that keeps this structure from decaying:
- Every relative link resolves to a document or a section index.
- Every anchor resolves to a heading the site renders, checked against the rendered HTML instead of 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.
tsc catches none of these, and none of them break the build. The reader finds them.