ADR-0015 — Every package and project lives at the root, in one shape and one look
Every package and project lives at the site's root, at its own short address, with the same folder shape, version lines and look.
- Status: Approved
- Date: 2026-10-31
- Deciders: @puncoz
- Approved by: @puncoz
- Approved on: 2026-10-31
- Supersedes: ADR-0013
decisions 1 (in part: the Documentation project's exemption), 3 (line names, the
nextline and channel), 5 (URLs), 6 (in part: the Documentation project's default layout) and 9 (in part: the line checks); ADR-0014 decisions 3 (in part: thenextchip becomesdev), 6 (in part: tokens scoped to the home page) and 9 (header) - Superseded by: ADR-0016 in part (decisions 8 and 9); ADR-0017 in part (decision 9); ADR-0030 in part (decision 7's "no entrance motion on docs pages", for the footer); ADR-0034 in part (its implementation's "the proxy acts only on catalogued paths")
- Reworded by: ADR-0038 (earlier names only)
- Design documents:
../app-flow/site-pages.mdx,../prd/site-pages.mdx; on implementationarchitecture/system.mdx,api/content-routes.mdx,state/package-version-lifecycle.mdx,app-flow/package-navigation.mdx,DESIGN.md
Context
ADR-0013 and ADR-0014 are built. Three things about the result are now wrong for the owner:
/docsmeans two things. Packages and projects sit under/docs/<slug>, and so does this site's own documentation (/docs/documentation). Next to them sit two scaffold pages ("Hello World" at/docs, "Components" at/docs/test). The owner wants a package at/<slug>, and this site's documentation treated as what it is: maintainer documentation for the site itself.- Docs pages don't look like the site. The home page has its own visual system (
DESIGN.md: graphite and teal, Bricolage Grotesque, Onest, JetBrains Mono), scoped to.home. Docs pages still wear Fumadocs' neutral theme and Inter. There is no light/dark switch on the home page; docs pages have Fumadocs' own. - Versions need a permanent home for unreleased work, and names the package chooses.
ADR-0013 has a
next/line created for an unreleased major and renamed on release, and fixes line names tov<major>. The owner wants one permanentdevline for work in progress, and released lines named by whatever string the package supplies (v1.0,2024-lts, …), configured in a file.
Facts this decision rests on, read from the installed code:
- Fumadocs'
loader()takes abaseUrl(src/lib/source.tspassesdocsRoute = '/docs').DocsLayout(fumadocs-ui 16.17.9) acceptsslots(header, sidebar, container), so the header can be replaced while the sidebar, contents panel, version menu and page footer stay.fumadocs-ui/layouts/shared/slots/theme-switchexportsThemeSwitchwithmode: 'light-dark' | 'light-dark-system'. The loader takes a slug function per page (SlugFn, andslugsFromDatain fumadocs-core 16.17.9), so a line's URL segment can come from itsmeta.jsonrather than its folder name. - Next.js route groups let
app/(home)/page.tsxown/while a required catch-allapp/(site)/[...slug]/page.tsxowns every other content path (dynamic-routes.md,route-groups.md). A required catch-all does not match/. The installed docs do not state that a static top-level route (llms.txt,static.json,og/,llms.mdx/) wins over a root catch-all; the implementation proves it with a build before relying on it, and the reserved-slug check below keeps a content folder from ever claiming those names. - Nothing has been deployed (owner, 2026-10-31), so no
/docs/…URL needs a redirect. - No package or project is documented yet: moving folders and URLs now costs only this site's own pages and the references to them.
Decision
Routes and folders
-
Every package and project lives at the root:
/<slug>. There is no/docsprefix. The folder path is the URL path:Folder URL content/<slug>/index.mdx/<slug>— overview, with the package hero (decision 7)content/<slug>/use/<folder>/<page>.mdx/<slug>/use/<line>/<page>— user docs, versioned;<line>comes from the line'sname(decision 4)content/<slug>/use/<page>.mdx/<slug>/use/<page>— user docs of an unversioned projectcontent/<slug>/maintain/<page>.mdx/<slug>/maintain/<page>— maintainer docs, not versioned— /<slug>/use→ thelatestline's first page, ordev's when nothing is released— /<slug>/use/latest/<page>→ 307 to the same page in thelatestline— /<slug>/….md— the page as Markdown— /og/<slug>/…/image.png,/llms.mdx/<slug>/…/content.md— internal, as todayThe folder path is the URL path everywhere except a line's segment, which is its configured name. The collection root moves from
content/docs/tocontent/.docsRoutebecomes''(root), and the docs page moves fromapp/docs/[[...slug]]/toapp/(site)/[...slug]/, with its layout inapp/(site)/layout.tsx. -
Slugs are checked. A root folder's name matches
^[a-z0-9][a-z0-9-]*$and is none of the reserved names:api,auth,callback,sign-in,sign-out,og,search,static,assets,public,_next. The build fails otherwise. Names with a dot (llms.txt,static.json,llms.mdx) can't match the pattern. A new top-level route adds its name to the list in the same change. -
The shape of every entry (ADR-0013 decisions 2, 4 and 7 stand; lines change):
content/<slug>/ ├── meta.json # title, description, catalog (ADR-0013 decision 1) ├── index.mdx # overview → /<slug> ├── use/ # user docs │ ├── meta.json │ ├── meta.json # optional "pages": the order of older lines in the version menu │ ├── dev/ # work in progress on the default branch: always present │ │ ├── meta.json # {"channel": "dev", "version": "2.0.0-dev.4"} │ │ └── … │ ├── v1.0/ # one folder per released line; any folder name │ │ ├── meta.json # {"name": "v1.0", "version": "1.0.3", "channel": "latest"} │ │ └── … │ └── lts-2024/ # an older line │ ├── meta.json # {"name": "2024 LTS", "version": "0.9.3", "channel": "legacy"} │ └── … └── maintain/ # maintainer docs: the default branch, never versioned ├── meta.json └── … # architecture, local setup, tests, releasing, contributing, decisions -
Version lines: one
dev, and released lines named by the package.devdocuments the default branch: work in progress, not yet released. Every versioned package has it, inuse/dev/, at/<slug>/use/dev/…. Itschannelisdev, its banner says "In development", and itsversionis the manifest's version on the default branch. Its name is alwaysdev.- A released line's name is any string the package supplies, set as
namein the line'smeta.json:v1.0,1.x,2024 LTS. The name is what readers see in the version menu, the banners, search results andllms.txt. The line's URL segment is the name made URL-safe: lowercased, spaces to hyphens, anything else outside letters, digits,.,_and-removed (2024 LTS→2024-lts,v1.0→v1.0). The folder name is free and never appears in a URL. Withoutname, the folder name is used. versionis the exact release the line documents, read from the package's manifest, andchannelislatestorlegacy. Exactly one released line islatest.- Order in the version menu:
latest, thendev, then older lines in the order ofuse/meta.json'spages, or byversion(natural order, newest first) when it isn't set. - Releasing copies
use/dev/into a new line folder with itsname,versionandchannel: "latest", and turns the previouslatesttolegacy(or updates the current line in place, when the package keeps one line per range).devkeeps going. A legacy line may be deleted when it is no longer supported; its URLs then redirect tolatest(ADR-0013's rule stands). - Unreleased package: only
dev. Unversioned project: no lines; itsuse/holds pages directly. A project that ships releases may use lines like a package.
-
The build checks the shape (ADR-0013 decision 9, extended): a missing or invalid
catalog; aprivateentry; a bad or reserved slug; a line withoutversionorchannel; adevchannel anywhere butuse/dev/, oruse/dev/with another channel; a released line whose URL segment is empty, isdevorlatest, ends in.md, or matches another line's; twolatestlines;legacylines without alatest.
This site's own documentation
- The site is the catalogued project
docs. Its folder iscontent/docs/(overview at/docs, "what this site is and how it is organised"), withcatalog: { kind: "project", visibility: "public" }. Today's Documentation tree becomes its Maintain track:content/docs/maintain/{adr,architecture,app-flow,prd,api,dfd,sequence, state,erd,rbac,security,_templates}at/docs/maintain/…. It has no Use track yet; one holding the consumer guide (PROJECT_AGENTS.mdas pages) is left for a later change. It is listed in the catalogue and on the map like any project, and thedocumentationexemption in the catalogue plugin is removed. The scaffold pagescontent/docs/index.mdx(Hello World) andcontent/docs/test.mdxare deleted.
One look across the site
-
Docs pages use the home page's visual system, quietly (
DESIGN.md):- The tokens move from the
.homescope to the whole site, and Fumadocs'--color-fd-*variables are mapped onto them, so the sidebar, contents panel, callouts and code blocks take the graphite and teal palette in both themes. - Headings in Bricolage Grotesque, body text in Onest, code, package names and versions in JetBrains Mono, loaded once in the root layout.
- No orbit and no entrance motion on docs pages: reading pages stay still.
- Package hero on each overview (
/<slug>): a band above the page's own content, built from the catalogue. It holds the ecosystem dot and colour (outlined for a project), the name in monospace, the description, thelatestversion anddevon a dashed tag, a small version rail of the lines, and links: Use, Maintain, registry, repository. A project without lines shows Use and Maintain only.
- The tokens move from the
-
One header on every page, replacing ADR-0014 decision 9's home-only header and Fumadocs' own: the wordmark, a search trigger (
⌘K, opening the site search dialog), Packages, Projects (only when a project exists) andllms.txt, then GitHub and the theme switch. On docs pages it fillsDocsLayout's header slot; the sidebar, version menu and Use | Maintain switch stay below it. -
Light and dark on every page. The header carries Fumadocs'
ThemeSwitchinlight-darkmode. Dark stays the default (RootProvider theme={{ defaultTheme: 'dark' }}); the reader's choice is kept by Fumadocs' theme provider in their own browser and applies to the home page and docs pages alike. The orbit map, rails and terminal read the tokens, so they follow the switch.
Options considered
| Concern | Option | Pros | Cons | Verdict |
|---|---|---|---|---|
| URLs | A — /<slug> at the root | Short, memorable URLs; the owner's choice | Slugs share the root with site routes; needs the reserved list | Chosen (owner) |
| URLs | B — keep /docs/<slug> | No route change | /docs keeps meaning two things | Rejected |
| Folders | A — content/<slug>/, folder path = URL | One rule to explain; the site's folder is content/docs/ | Moves the collection root; consumers' guide changes | Chosen (owner) |
| Folders | B — keep content/docs/<slug>/ | No move | The site's folder becomes content/docs/docs/; paths no longer match URLs | Rejected |
| Site docs | A — Maintain track of the listed project docs | No special case; the site follows its own rules | Its pages move; ~170 references to update | Chosen (owner) |
| Site docs | B — Maintain track, unlisted | Not presented as a project | Needs an unlisted flag | Rejected |
| Site docs | C — not published, repository only | Smallest site | Supersedes ADR-0002; no rendered diagrams | Rejected |
| Unreleased work | A — permanent dev line | Always a place for work in progress; releasing is a copy | One more folder per package | Chosen (owner, named dev) |
| Unreleased work | B — next/ renamed on release (ADR-0013) | Fewer folders between majors | Unreleased minor work has nowhere to go | Superseded |
| Released line names | A — name in the line's meta.json, any string; URL segment derived from it | The package chooses its own scheme (v1.0, 2024 LTS); one place to set it; folder names don't matter | Order can't come from the names, so it comes from pages or version; renaming changes URLs | Chosen (owner) |
| Released line names | B — the folder name is the name | Nothing to configure | Folder names must be URL-safe and can't carry a display name like 2024 LTS | Rejected |
| Released line names | C — a fixed scheme (v<major>, v0.<minor>) | Predictable, checkable | Doesn't fit packages with other schemes | Rejected (owner) |
| Maintainer docs | A — not versioned | Maintainers work on the default branch | Old lines' setup notes aren't kept | Chosen (owner) |
| Docs look | A — same system, quiet, plus a package hero | One site; overviews say what a package is at a glance | Fumadocs variables to map and check in both themes | Chosen (owner) |
| Docs look | B — tokens and fonts only | Smallest change | Header and docs pages don't match the home page | Rejected |
| Theme | A — ThemeSwitch in the shared header, dark default | Installed component; one place | Light theme of the docs pages needs its own contrast check | Chosen |
| Theme | B — light / dark / system | Follows the OS | A third state the owner didn't ask for | Rejected |
Consequences
Positive
- One rule for every URL and folder:
/<slug>iscontent/<slug>/, and the shape inside is the same for packages, projects and the site itself. - Work in progress always has a line (
dev), releasing never renames a folder, and each package names its lines its own way. - Docs pages and the home page read as one site, in either theme.
Negative / accepted trade-offs
- Every reference to
content/docs/documentation/and/docs/documentation(AGENTS.md, PROJECT_AGENTS.md, README, the skills index, templates, every ADR's links) changes in one pass. - The root URL space is shared: every future top-level route must be added to the reserved list.
- Changing a line's
namechanges its URLs; old links then 404 unless the line is removed (which redirects tolatest). The consumer guide says to treat names as permanent.
Content invariants
- §6.1 Private stays private: unchanged; the schema still refuses a non-
publicentry. - §6.3 Every package page states its version: kept;
devpages say "In development" with the default branch's version. - §6.5 Every page is readable as Markdown: kept at the new paths (
/<slug>/….md),llms.txtandllms-full.txt.
Security considerations
- No new data, endpoint or secret. The pages, Markdown, OG images and search records are the same public content at new URLs.
- Search index: record URLs change. The index is rewritten on the owner's next
bun run build; until then, live results would point at old URLs. Nothing is deployed, so no reader sees that gap. - Reserved slugs keep a content folder from shadowing a future security-relevant route
(
/api,/auth,/sign-in,/callbackfor the WorkOS ADR). - Theme choice is stored by the theme provider in the reader's browser (
localStorage): a preference, not personal data, never sent anywhere.
Implementation notes
- A test build proving a root required catch-all coexists with
/,/llms.txt,/llms-full.txt,/static.json,/og/…and/llms.mdx/…. - Move
content/docs/documentation/→content/docs/maintain/; writecontent/docs/meta.json(catalogue entry) andcontent/docs/index.mdx(overview); delete the two scaffold pages. Collection rootcontent/. -
src/lib/shared.ts: root routes;src/lib/source.ts,catalog-routes.ts,src/proxy.ts: new paths, line URL segments fromname(a loader slug function), thedevline,/<slug>/uselanding. The proxy acts only on catalogued paths (isEntryPath). -
catalog-plugin.tsandcatalog-schema.ts:namefield,devchannel in place ofnext, slug and line checks, version-menu order, thedocumentationexemption removed.llms.txtlabels a line by its name. - The home page: the
nextchip becomesdev(aliasesnext,unreleased); rows and rails showdev. -
app/(site)/[...slug]/andapp/(site)/layout.tsx(Fumadocs notebook layout, whosenav.componenttakes the site header: the docs layout's own header is mobile-only); OG, Markdown andllmsroutes on the new paths.src/lib/layout.shared.tsxremoved, unused. - Site-wide tokens and fonts; Fumadocs variable mapping; a shared
SiteHeaderwith the search trigger andThemeSwitch, used by the home page and the docs layout's header slot. - The package hero on overviews.
- Contrast check of docs pages in both themes, computed from the tokens: every text pair is 4.5:1 or more except the active sidebar link on the page colour in light (4.29:1), so the phone drawer now sits on the panel colour (4.61:1), like the desktop sidebar. impeccable's detector on the source: its one finding, a 3px side stripe on the hero, was removed. On the built HTML it could not load the stylesheets, so its one finding there (flat type sizes) is not a real result. Checking in a browser, the theme switch included, waits for the owner's yes.
- Rewrite every reference to the old paths.
- Tests — manual checks until ADR-0007 picks a runner, recorded 2026-10-31 and 2026-11-01
with sample entries copied into
content/and removed afterwards:- a scratch
bun testpassed 9 tests (47 assertions): every build-check failure, including a reserved slug, a bad slug, a root page, a misplaceddev, a bad or duplicate segment and pages under a versioneduse/;lineSegment,lineInfoandpageVersion; menu order with and withoutpages; the chip filter and catalogue filter withdev. - on
next start, all as expected:/lodestar/useand/lodestar/use/latest/commandsgave 307 tov0.1,.mdkept;/ledgerly/use/v3gave 307 to4.x;/ledgerly/use/3.x-lts(a line named3.x LTS) gave 200;/docs,/docs/maintain/adrand their.mdgave 200 (text/markdownfor.md);/nothingand/docs/documentationgave 404;/llms.txt,/static.json,/og/<slug>/image.pngand/llms.mdx/<slug>/content.mdresponded;- the rendered HTML had the hero tags, the version menu descriptions and both banners, and the theme toggle on home and docs pages.
- a scratch
-
bun run types:checkandbunx next buildpass (171 pages without samples).bun run lintstill fails: "typescript-eslint does not support TS 7.0". Reported, not fixed here.
Docs to update on implementation
-
../app-flow/site-pages.mdx— to match the build -
../prd/site-pages.mdx -
../app-flow/package-navigation.mdx,../app-flow/home-page.mdx— new URLs, header, theme -
../state/package-version-lifecycle.mdx— thedevline, named lines, release by copy -
../architecture/system.mdx,../api/content-routes.mdx,../dfd/system.mdx— routes -
DESIGN.md— tokens site-wide, docs pages, package hero, header, theme switch - ADR-0002 and ADR-0010 (both Proposed) — revised in place for the new location and paths;
../app-flow/project-root-folders.mdxremoved, since no sidebar switcher of projects remains -
AGENTS.md,PROJECT_AGENTS.md,README.md,.claude/agent-skills/INDEX.md— paths, shape -
./index.mdx— index row, next free number -
./index.mdx— status on approval
Open questions
None. Line names and the dev line are the owner's (2026-10-31).
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.
ADR-0016 — Home search is the Fumadocs dialog, and private projects render server-side
The home page's search button opens the site's one search dialog, and its private projects are rendered on the server, with Cache Components on.