How routing works here

Why the routes are laid out the way they are, and what does the work for you.

In short

  • Folders are the routes, and the routes are the only list. The docs nav, breadcrumbs, tab titles and section pages are all built from the route tree and each route's staticData. Nothing else lists pages, so nothing can fall out of step.
  • An experiment is mostly docs. A prototype or feature lives in one folder. Its docs sit at its own URL (/prototypes/baseline) inside a pathless _docs/ layout; the thing itself runs under the reserved segment app/ (/prototypes/baseline/app), with its own chrome and nothing of the docs around it.
  • Experiments are sealed off from each other. Each folder may import only itself and the shared platform. Reusing another experiment's code means copying it. A commit with a cross-import is refused.
  • Mistakes fail early. Missing titles, a breadcrumb step with no page, or app in the wrong place are type errors; a cross-import is a Fallow error, both before every commit.

The shape of an experiment

src/routes/prototypes/baseline/
  route.tsx            names the experiment ("Baseline")
  _docs/route.tsx      the docs shell: header, docs nav, page (adds nothing to the URL)
  _docs/index.tsx      /prototypes/baseline           the docs start page ("Overview" in the nav)
  _docs/$slug.tsx      /prototypes/baseline/status    the -docs/*.md files
  _docs/decisions.tsx  /prototypes/baseline/decisions its decisions, from -lib/decisions.ts
  _docs/rules/         /prototypes/baseline/rules/…   a real group: route.tsx, index.tsx, pages
  _docs/components/    /prototypes/baseline/components/…
  app/route.tsx        /prototypes/baseline/app       the running prototype: session, its own chrome
  app/_public/…        /prototypes/baseline/app/search
  app/admin/…          /prototypes/baseline/app/admin/…
  -lib/                code: components, rules, decisions, server functions
  -docs/               markdown for people: status, notes

Features have the same shape: _docs/ for docs, app/ for the running feature (the image grid demo, the page builder's admin and site, the order board, the decks, sample receipts).

TanStack's file names

TanStack Router turns files into routes by their names (file naming conventions).

NameMeansWhy we use itExample here
__root.tsxThe root route around every pageThe HTML shell, and the tab title and description built from the trailsrc/routes/__root.tsx
route.tsxThe route for its folder's own URL, usually a layout around the pages insideIt names a section: its title is the breadcrumb and nav group labelprototypes/baseline/route.tsx
index.tsxThe page at the folder's URL exactlyEvery section has one, so every breadcrumb opens a page; the nav calls it "Overview"_docs/rules/index.tsx
$nameA URL parameterMarkdown docs, rule charts, component pages, orders_docs/rules/$chartId.tsx
_nameA pathless layout: wraps its pages in a component without adding to the URL_docs puts the docs shell around docs pages; _public and _checkout give app pages their chrome/features/receipt/variations, not /features/receipt/_docs/variations
-nameIgnored by the routerCode (-lib) and markdown (-docs) sit next to the routes that use themprototypes/baseline/-lib/
a.b.tsxFlat nesting: b under aOnly for a page plus its PDF, kept side by sideapp/receipt.$orderId.pdf.ts
(name)A route group: folder without a URL segment or layoutNot used. Parentheses are risky in globs (see below)—

Our conventions, and why

One folder per experiment. Experiments may test ideas that contradict each other, so each has its own code, server state and docs. Nothing has to stay compatible across them.

Docs by default, the app under app/. Most of an experiment's pages are docs, so docs get the experiment's own URL. The running thing gets one reserved segment, app. That gives every kind of file one pattern that tools can select:

FilesPattern
Docs pagessrc/routes/docs/**, src/routes/*/*/_docs/**
Running appssrc/routes/*/*/app/**
Experiment codesrc/routes/*/*/-lib/**
Experiment markdownsrc/routes/*/*/-docs/**

Rules for Claude (.claude/rules/*.md with paths:), hooks (if: "Edit(**/_docs/**)"), lint overrides and doc lists all select files by patterns like these, so a stable naming scheme beats a list of exceptions. _docs and app are plain characters in every glob engine, regex and shell; parentheses aren't.

_docs is pathless, app is not. The docs shell (header, nav) wraps docs pages but shouldn't add /docs to their URLs, so it's a pathless layout. The app needs a real URL segment, because the docs start page already owns the experiment's URL.

app is reserved, and apps are on their own. app is used for nothing else. App pages never show in the docs nav or breadcrumbs, and their tab titles name only the app ("Search · NTB Bilder og video"): a prototype in a user test shows only what its users would see. An app may build its own breadcrumb from its own routes. The facilitator's controls are behind the flask icon (prototype menu).

Every breadcrumb opens a page. Every URL prefix outside an app is a page: a section has route.tsx (its name) and index.tsx (its page). Section pages that only list their pages use SectionIndex, which reads the same route tree as the nav.

Nesting only for real groups. Markdown docs sit at the experiment's root (/prototypes/baseline/decisions). A folder appears only when pages belong together (rules, components, foundations).

Inside apps, URLs copy the real site. Prefixes inside app/ (asset, confirmation, admin/product-config) don't need pages, and apps may redirect to their entry page (app/admin to the first admin page, app/site to /site/nb).

Silos: what an experiment may use

Experiments often test ideas that contradict each other, so each one is sealed off.

FolderWhat's in itWho may import it
src/routes/<kind>/<id>/One experiment: its docs, app, code and markdownOnly itself
src/platform/Neutral building blocks: UI components, formatters, the media fixtures, the docs shell and nav, the chart engine, the design tokensEvery experiment
src/reference/im-web today, team proposals, and the working model (the business rules experiments start from)Only the Docs pages; experiments copy what they need
src/routes/docs/, the root routesDocs and the lists of experimentsNobody
  • Copy, don't import. bun scripts/copy-from.ts <source> <kind>/<target> <file> copies a file and everything it needs into -lib/from-<source>/ and notes the source commit in copied-from.json. Each experiment then owns its version and can change it freely.
  • The working model is the source. It lives in src/reference/working-model/. Baseline has its own copy in -lib/rules/; improving the shared model means changing the reference, then copying it again.
  • Checked by Fallow (.fallowrc.json). Every folder under src/routes/prototypes and src/routes/features is its own zone automatically. The check runs after each edit Claude makes and in the commit hook, so a cross-import never reaches the repo.
  • Before every commit (.githooks/pre-commit, turned on by bun install): formatting, lint with the type checker, and Fallow's check for cross-imports and new unused code. Existing unused code is recorded in fallow.baseline.json. bun run check also reports duplication and complexity, without stopping anything.

One source: staticData

Every route says what it's called. src/platform/page-meta.ts makes title and description required on every route:

staticData: { title: 'Credits lifecycle', description: 'How credits are bought, used up and expire.' }
  • title: null for routes that aren't a step: pathless layouts, redirects, PDFs.
  • order places a page among its siblings in the nav. The index page always comes first.
  • entries lists the params of a $param route, so the nav can link each one. docMeta(docs) does this for markdown docs.
  • Titles can be functions of the URL's params (a doc's heading, a chart's title), so the nav can label a link without loading the page.

From that one source:

WhatBuilt by
Tab title ("Credits lifecycle · Rules · Baseline") and meta descriptionhead() in __root.tsx, from the matched routes
Breadcrumbs, h1 and lead on docs pagesPageHeader in DocsPage, from the matched routes
The docs navdocs-nav-tree.ts, walking the router's route tree
Section pagesSectionIndex, from the same tree

What catches mistakes

MistakeCaught by
A route without staticData (title and description)The type checker: StaticDataRouteOption requires them
A URL prefix outside an app that isn't a section with an index pageThe type checker: EveryPrefixIsAPage in src/platform/route-guard.ts names the paths
app used anywhere but /<kind>/<id>/appThe type checker: AppOnlyUnderAnExperiment, same file
A link to a route that doesn't existThe type checker: links are typed against the route tree
An experiment importing another experiment or the referenceFallow (.fallowrc.json), after each edit and before every commit
An experiment's instructions for Claude out of step with its decisionsbun test: generated-rules.test.ts (run bun scripts/generate-rules.ts)
A $param docs route without entries, a page with pages under it but no route.tsxA warning in the browser console in dev ([docs nav])

Lint, the type checker, the tests and Fallow run before every commit, on what is staged (.githooks/pre-commit), and bun run build runs lint first, so none of these can reach the repo or a build.