Four days ago we shipped 50 pages, one per named assessment, as a trial. This weekend we did the other 281. The site now has 331 of them, one per test, across 56 vendors, at /games/<vendor>/<test>, and the live sitemap went from 530 URLs to 835.
The content is not the interesting part. The geometry is.
One file per vendor, because 31 branches had to land in one directory
All 331 pages come out of one registry, and that registry is 59 files in a directory with an index that does nothing but concatenate them:
/**
* Every per-test page, one content file per provider.
*
* One file per provider (two for TestGorilla) so that the workers filling them
* in never edit the same file. Everything that needs to know about these pages
* (the routes, the sitemap, the provider hubs' link lists, the guard test)
* reads from here.
*/
export const TEST_PAGES: TestPageEntry[] = [
...acerTestPages,
...alvaLabsTestPages,
...amberjackTestPages,
...
];
59 files, 40,183 lines, 2.38 MB of TypeScript. The index adds one import line and one spread line per file, which means two branches working on two vendors produce a conflict only in the import block, where git resolves both sides cleanly because they are different lines in alphabetical order. 18 named branches, 10 unnamed worktree branches and 3 recovery branches merged into the integration branch over an evening, and the conflicts were all in that index.
This is a rule we arrived at the hard way: if parallel work has to touch a shared file, make the shared file a list of one-line entries. Anything richer, and you spend the evening resolving.
Three workers were interrupted, and we committed their work anyway
Partway through, three of the workers stopped mid-file. Not failed, not finished: interrupted. The run that recovered them made three commits with the same message, in the same second:
4abfafa2 WIP: interrupted game-pages worker (unreviewed, unfinished)
lib/games/test-pages/faa-atsa.ts | 815 +++++++++++++++++++++++-
dd57670e WIP: interrupted game-pages worker (unreviewed, unfinished)
lib/games/test-pages/criteria.ts | 497 ++++++++-------
lib/games/test-pages/revelian.ts | 323 +++++++-----
lib/games/test-pages/test-partnership.ts | 593 ++++++++++--------
c743dac2 WIP: interrupted game-pages worker (unreviewed, unfinished)
lib/games/test-pages/harver.ts | 1054 ++++++++++++++++++++++++++-
The commit message is the whole technique. A dirty worktree from an interrupted job is a thing you are afraid to touch, and so is a branch whose state nobody can describe. A commit that says "unreviewed, unfinished" in its subject line is neither: it is a checkpoint you can diff, review, fix, or throw away, and nothing silently merges it while you are not looking.
The part I did not expect: all three were nearly shippable. Measured from the WIP commits to today's main, those five files took 22 insertions and 20 deletions between them to go live. faa-atsa.ts needed exactly one changed line out of 815.
That is not an argument that unreviewed output is fine. It is an argument for committing it before you judge it, because the alternative, on the night, is deleting 2,000 lines you would have kept.
331 route files, all exactly ten lines
Every page's route file is this, and there are 331 of them:
import { TestPageShell, testPageMetadata } from '@/components/games/TestPageShell';
import { requireTestPage } from '@/lib/games/test-pages';
const entry = requireTestPage('aon', 'gapchallenge');
export const metadata = testPageMetadata(entry);
export default function Page() {
return <TestPageShell entry={entry} />;
}
Normalise away the two string arguments and all 331 files hash to the same value. That is deliberate, and requireTestPage is the reason it works: it throws rather than returning undefined, so a typo in a route file is a build failure and not a page that renders an empty shell in production.
A dynamic [test] segment would have been fewer files. We had one, for the /tests cluster, and deleted it last week. Static folders make the route space enumerable, which is what the guard test needs and what the next section is about.
The failures a content registry has, and the near miss
* Nothing renders these pages until someone visits them, so the mistakes that
* matter are invisible in review: a registry entry with no route folder (no
* page, but a sitemap URL that 404s), a folder with no entry (a build that
* throws), a game id from another provider, a title that blows the length
* budget, or a path that robots.txt blocks because a game route happens to be
* its prefix.
That last one nearly happened. Our robots.txt is 850 lines, of which 780 are plain Disallow prefixes, and 372 of those are the individual practice game routes that sit directly under /games/. Eight of the new page slugs are byte-identical to one of those disallowed ids:
/games/arctic-shores/analysis
/games/arctic-shores/arrows
/games/arctic-shores/balance
/games/arctic-shores/order
/games/arctic-shores/patterns
/games/arctic-shores/team-sales
/games/arctic-shores/tickets
/games/arctic-shores/token-search
Disallow: /games/balance does not prefix-match /games/arctic-shores/balance, so all eight are crawlable. They are safe entirely because of which segment the word sits in. One rule written as /*balance instead and eight pages quietly leave the index, which is why the test runs the real robots() export over the real route list rather than trusting anybody's reading of it:
const blocker = plainPrefixes.find((prefix) => route.startsWith(prefix));
expect(blocker, `${route} is blocked by "${blocker}"`).toBeUndefined();
The other check worth copying is the one for internal links. Each entry has a related list, 855 links across the set, and the test resolves each href to a folder on disk and asserts a page.tsx exists. Writing a link to a page a parallel branch has not created yet is the single most common mistake in this kind of round.
The ledger
Counts I pulled out of the registry by importing it rather than grepping it, because regex counting over TypeScript has lied to me twice:
- 331 pages over 56 vendors, 2 to 17 pages each, median 6
- 1,971 source URLs, 804 of them distinct, over 208 hosts. The biggest single host is a web archive with 99
- one page rests on one source, another rests on 22
- 1,857 fact rows, 1,649 sections, 1,277 FAQs, 197,861 words
- 1,943 keywords, 1,934 distinct, so nine collisions in all
- every
metaTitlebetween 33 and 48 characters, every description between 124 and 158, enforced rather than trimmed - 359 references to practice games, 358 distinct, which leaves 14 of our 372 games with no page of their own and 15 pages covering more than one game, because some vendors sell levels of a test that candidates search for as one thing
See it
/games/aon/gapchallenge is one of them; the index of a vendor's set is its hub, for instance /games/arctic-shores.
The contrast worth checking yourself is what happens when you make a path up. Ask for /games/aon/zzz and you get a real 404 with noindex, follow, because that URL space is 331 static folders and nothing else. Ask for /games/zzz, one segment up, and you get 200 with the title "Game Not Found" and two conflicting robots tags in the same document, because that space has a dynamic segment which reads the session and commits its head before the body can decide the game does not exist. Same site, two URL spaces, and only one of them tells a crawler the truth. The one-segment case is on the fix list and the folders are the reason the new one never had the problem.
The machine-readable summary of the whole library is at cogniprep.app/llms.txt, which is generated now rather than typed, and its first line will tell you the same 372 and 56 this post does.
Top comments (1)
The WIP commit works because the message moves the state from the worktree, where only one process can see it, into history, where everything can. One more step makes it load-bearing: a CI check that rejects any commit matching your WIP pattern from reaching the release branch. Without that, "unreviewed, unfinished" is documentation, and documentation rides quietly to production on the next innocent merge. With it, the label is enforced, and the checkpoint can only move forward through a deliberate message rewrite - which is the review gate you wanted.
The 22-lines number is the part I'd steal first. "Committing interrupted work was safe" is usually unfalsifiable; measuring the delta from WIP to main made it a fact. Worth recording that measurement next to the commit, because it's also the calibration for the next interruption: if the delta is usually 22 lines, a future WIP needing 2,000 is the anomaly that deserves a second look.
And requireTestPage throwing instead of returning undefined is the quiet hero. Fail-closed at lookup is what lets 331 identical files stay identical safely.