ADR-0013 — Packages and projects share one catalogue shape: two tracks, version lines and demos
Packages and projects declare one catalogue entry, with a Use track, a Maintain track and version lines, so every entry is built the same way.
- Status: Approved
- Date: 2026-10-31
- Deciders: @puncoz
- Approved by: @puncoz
- Approved on: 2026-10-31
- Supersedes: —
- Superseded by: ADR-0024 (in part:
decision 4's last sentence, a project has at most
devand one released line) - Design documents:
../app-flow/package-navigation.mdx,../state/package-version-lifecycle.mdx,../prd/home-page.mdx,../architecture/system.mdx,../dfd/system.mdx,PROJECT_AGENTS.md
Context
ADR-0010 gives every package and project a root folder, content/docs/<project-slug>/, but
leaves open what goes inside it (AGENTS.md §1). The home page (ADR-0014) lists the whole
collection, so it needs that answer first. Four facts shape it:
- Packages have versions. A package can have several released versions in use and an unreleased one in progress. Invariant 3 requires every package page to say which version it documents.
- Two kinds of reader. People who use a package or project need install steps, guides and APIs. The people who maintain it need architecture, local setup, testing and the release process. The owner decided on 2026-10-31 that each project has both, as two tracks.
- Demos. Some packages, npm ones in particular, need a working demo next to their docs.
- Private projects. The site will hold private projects behind sign-in (WorkOS, in a later ADR). The owner decided on 2026-10-31 that a signed-out visitor sees nothing of them: no name, no count, no placeholder.
What Fumadocs does today, read from the installed source:
getSidebarTabs(fumadocs-ui/dist/components/sidebar/tabs) turns every folder withroot: true, nested ones included, into one entry in a single flat switcher. It cannot show package → version → track as levels.DocsLayouttakes anytreeand an explicittabslist orfalse(fumadocs-ui/dist/layouts/docs/index.d.ts), so a package route can be given only its own subtree and its own switches.meta.jsonfiles are validated bymetaSchemainsrc/lib/source.ts, which can be extended.
Decision
-
Every root folder declares a catalogue entry in its
meta.json, validated by an extendedmetaSchema:{ "title": "lodestar", "description": "Keyboard-first command palette for React.", "root": true, "catalog": { "kind": "package", "ecosystem": "npm", "visibility": "public", "repository": "https://github.com/example/lodestar", "registry": "https://www.npmjs.com/package/lodestar", "featured": false } }kindispackageorproject.ecosystemis required for packages:npm,composer,pypi,go,cargoorother.visibilityaccepts onlypublicuntil the private-documentation ADR addsprivate, so the build fails on a private entry instead of publishing it.featuredputs a package with a demo forward on the home page. The Documentation project has nocatalogand is not listed. -
Two tracks in every root folder:
use/andmaintain/. Use is for people using the package or project: install, guides, API, demo. Maintain is for the people who look after it: architecture, local setup, tests, releasing, contributing, its own decisions. A track may be missing; the switch then hides it. For a public package or project both tracks are public. For a private project both will be private. -
A package's Use track has one folder per documented major version, called a line:
use/v3/,use/v2/, anduse/next/for an unreleased major. Each line'smeta.jsonsetsversion(the exact version it documents, read from the package's manifest, such as3.2.0or4.0.0-beta.3) andchannel:latest,nextorlegacy. A released package has exactly onelatestline and at most onenext. A package with onlynextis unreleased. The lifecycle is in../state/package-version-lifecycle.mdx. -
The Maintain track is not versioned. It describes the repository's default branch. Projects are unversioned by default: their
use/holds pages directly. A project that ships releases may use lines like a package. -
URLs follow the folders:
/docs/<slug>is the overview (the root folder'sindex.mdx);/docs/<slug>/use/<line>/…and/docs/<slug>/maintain/…are the tracks./docs/<slug>/use/latest/…is a stable alias that redirects to the line markedlatest, so READMEs and registry pages can link without going stale. The redirect lives insrc/proxy.ts, which already rewrites/docs/…paths. -
Navigation inside a package is scoped. On package and project routes,
DocsLayoutreceives only the current track and line as its tree. The top of the sidebar holds a track switch (Use | Maintain) and a version menu that keeps the reader on the same page when it exists in the chosen line. Pages in anextline open with an "Unreleased" banner and pages in alegacyline with an "Older version" banner. Each banner names the exact version and links to the same page inlatest. The site-wide switcher of root folders is replaced by the home page catalogue and search; the Documentation project keeps its default layout. -
Demos are pages in a line,
use/<line>/demo.mdx, that embed a self-contained HTML file built by the package's own repository, through ADR-0011's sandboxed embed. The file lives atpublic/demos/<slug>/<line>/index.html, so each line demos the version it documents. This decision depends on ADR-0011. Until ADR-0011 is approved, no demo is published. -
Search and
llms.txtcarry the shape. Each Algolia record gainspackage,kind,ecosystem,track,lineandchannel. Search rankslatestand Maintain pages first and labelsnextandlegacyresults. Each page's Markdown opens with the version it documents.llms.txtlists every line, labelled. -
The build checks the shape and fails on: a missing or invalid
catalog; aprivateentry; twolatestor twonextlines; released lines without alatest; a line without aversion.
Decisions 2 and the signed-out rule in Context are the owner's (2026-10-31). Decisions 1, 3–9 are proposed here for the owner to confirm.
Options considered
| Concern | Option | Pros | Cons | Verdict |
|---|---|---|---|---|
| Audiences | A — two tracks per root folder | One place per project; a reader switches tracks without losing context | Each folder has two trees to keep current | Chosen (owner) |
| Audiences | B — tracks, Maintain always private | Internal notes never public | Outside contributors can't read how to contribute or release | Rejected (owner) |
| Audiences | C — a separate signed-in maintainer hub | Works across projects | A project's two kinds of docs live apart | Rejected (owner) |
| Versions | A — a folder per major line, plus next | Matches semver: pages change at majors; Fumadocs needs nothing new; a line can be deleted cleanly | Minor-version differences inside a line are noted in the text | Proposed |
| Versions | B — a folder per exact version | Every release exactly preserved | Dozens of near-identical copies; search full of duplicates | Rejected |
| Versions | C — latest only, older versions from git tags at build time | No copies in the tree | A build step that checks out tags; consumers can't edit old lines in place | Rejected |
| Versions | D — a separate content source per version | Strong isolation | One defineDocs per version; heavy for a few packages | Rejected |
| Maintain versioning | A — unversioned, the default branch | Maintainers work on the default branch; one copy | Releasing notes for an old line live in that line's Use pages or the changelog | Proposed |
| Maintain versioning | B — versioned like Use | Exact for old branches | Duplicates rarely changing pages | Rejected |
| Demos | A — HTML built by the package repo, sandboxed embed (ADR-0011) | Each line demos its own version; the site takes no dependency on the package; isolated from the site | Depends on ADR-0011; each package repo must build a demo file | Proposed |
| Demos | B — the package as a site dependency, demo as an MDX component | Shares the site's React | A dependency (and an ADR) per package and version; versions collide in one node_modules | Rejected |
| Demos | C — third-party sandboxes (StackBlitz, CodeSandbox) | No build work here | A third party on every demo page; slow; needs a DFD crossing and an ADR | Rejected |
| Stable links | A — use/latest/… alias redirect | README links never go stale | One more rule in src/proxy.ts | Proposed |
| Stable links | B — none | Nothing to build | Links to v2 pages break when v2 is removed | Rejected |
| Navigation | A — scoped DocsLayout per package with track switch and version menu | Matches the shape; uses tree and tabs Fumadocs already accepts | Custom sidebar header to build | Proposed |
| Navigation | B — default flat switcher with nested root: true folders | No code | Every track and line becomes a separate entry in one long list | Rejected |
Consequences
Positive
- Every package and project reads the same way (product principle 2), and the home page can list them from data instead of hand-written links.
- Unreleased docs can be published early without misleading anyone: the banner and the label in search say so.
- A private entry cannot be published by mistake: the schema rejects it.
Negative / accepted trade-offs
- Consuming repositories must follow the folder shape;
PROJECT_AGENTS.mdchanges with this ADR. - Releasing a major means renaming
nexttov<major>and movinglatestin one change. - Sidebar header, version menu, banners, alias redirect and build checks are new code.
- Demos wait for ADR-0011.
Content invariants
- 1 Private stays private: kept, and enforced by the build (decision 1, decision 9). Nothing
private enters
content/docs/until its own ADR. - 2 Checked against the code: each line's
versionis read from the package's manifest by the consuming repository. - 3 Every package page states its version: the line's
versionis shown in the page header, in the banner and in the Markdown version. - 4 Examples run: each example belongs to a line and runs against that line's version.
- 5 Readable as Markdown: unchanged; lines and tracks are ordinary pages.
Security considerations
- Private content. The schema accepts
publiconly. The future private-documentation ADR must keep private entries out of the build output, the search index,llms.txt,static.json, per-page Markdown and Open Graph images, and return the same 404 for an unknown and a private URL to a signed-out visitor, so a URL does not confirm a project exists. - Search records. The new fields (
package,kind,ecosystem,track,line,channel) are public metadata about public pages; nothing else is added. Readable by anyone with the search-only key, like every record. - Demos. Third-party code built by a package repository runs only inside ADR-0011's sandboxed frame, with no access to the site's origin, cookies or storage.
- Alias redirect. It maps
use/latestto a line folder name from the build's page tree, never to a value taken from the request, so it cannot be turned into an open redirect.
Implementation notes
- Extend
metaSchemawithcatalog,versionandchannel:src/lib/catalog-schema.ts, a Standard Schema around Fumadocs'metaSchema, used bysrc/lib/source.ts. - A catalogue helper in
src/lib/that reads root folders into typed entries (kind, ecosystem, lines, tracks, demo) for the home page, search and navigation:src/lib/catalog.ts. - Scoped
DocsLayoutfor package and project routes: track switch, version menu, banners. Typed root folders fromsrc/lib/catalog-plugin.ts;src/components/docs/. -
src/proxy.ts: theuse/latestalias and the redirects for removed lines (src/lib/catalog-routes.ts). - Search records in
src/app/static.json/route.ts: the new fields, and a scopetagper page. Ranking is not applied: it needs a change to the live index settings, which waits for the owner. -
llms.txtand per-page Markdown: the version line. - Build checks (decision 9), in the loader plugin; the build fails with one line per problem, naming the file.
- Tests — manual checks until ADR-0007 picks a runner:
bunx next buildwith sample content (three entries, kept out ofcontent/docs/) passes. Each failure stops it with its message: twolatestlines, aprivateentry, a root folder withoutcatalog, a line withoutversion,legacywithoutlatest, and a package withoutecosystem.- Two
nextlines, a line withoutchannel, a non-https URL and line order were checked against the functions with a throwawaybun testscript outside the repository: 6 tests, 32 assertions, all passing. - On
bunx next start:use/latest/…and.mdgive 307 to the latest line, a removed line (v9/…) gives 307 to the same path in latest, real pages give 200, and an unknown package gives 404. Markdown starts with the "Documents" line;llms.txtlabels lines;static.jsoncarries the tags.
-
bun run lint,bun run types:checkandbunx next buildpass. Type check and build pass. Lint does not run:typescript-eslintdoes not support TypeScript 7. This predates this ADR, and fixing it needs its own ADR.
Docs to update on implementation
-
../app-flow/package-navigation.mdx— to match the build -
../state/package-version-lifecycle.mdx -
../architecture/system.mdx— the catalogue helper and scoped layout -
../dfd/system.mdx— the new search record fields -
../api/—llms.txt, per-page Markdown andstatic.jsoncontracts -
PROJECT_AGENTS.mdandAGENTS.md§1, §6, §10 — the folder shape consumers follow -
README.md— project structure -
./index.mdx— index row, next free number -
./index.mdx— status on approval
ADR-0012 — Every page in content/docs is MDX
Every documentation page is an MDX file written as Markdown first, so a page can use a component only where it does something Markdown cannot.
ADR-0014 — The home page is a search-first map of every release
The decision to make the home page a search-first map of every package, project and release, and which skill builds it.