We deleted our only dynamic content route to get fourteen folders, and not one URL changed
CogniPrep had five pages describing test formats, at /tests/numerical-reasoning and four siblings, served by one dynamic route: app/tests/[test-type]/page.tsx reading an entry out of a registry. That is the obvious shape
CogniPrep had five pages describing test formats, at /tests/numerical-reasoning and four siblings, served by one dynamic route: app/tests/[test-type]/page.tsx reading an entry out of a registry. That is the obvious shape, and for five pages it was fine. Last week the cluster went to fourteen pages, and the dynamic route is gone. Every URL is identical to what it was.
Git recorded the change honestly, as a rename:
.../page.tsx => components/tests/TestTypeShell.tsx | 249 ++-
The body of the dynamic route became a shell component, and fourteen folders now each hold an eleven-line file:
import { testTypeMetadata } from '@/components/tests/test-type-metadata';
import { TestTypeShell } from '@/components/tests/TestTypeShell';
// Content and metadata live in lib/tests/test-types.ts.
const SLUG = 'abstract-reasoning';
export const metadata = testTypeMetadata(SLUG);
export default function Page() {
return <TestTypeShell slug={SLUG} />;
}
Fourteen near-identical files is the kind of thing a reviewer flags as duplication. Here is what buying them gets you.
What a folder gives you that a dynamic segment does not
A page can stop sharing. The whole point of a registry-driven cluster is that every page has the same shape, and the day one page needs a different shape, a dynamic route makes you branch inside the shell for every other page's benefit. With a folder, the odd page out stops importing the shell and keeps its URL. Nothing else in the cluster notices.
The route is greppable. "Where does /tests/error-checking come from" has a filesystem answer, which matters more than it sounds in a codebase where 56 provider hubs, 180 employer guides and 107 articles all resolve through registries.
A missing page becomes a test failure rather than a 404 in production. The new guard test, in full, is two assertions in both directions:
it('has a route folder for every registry entry', ...)
it('has a registry entry for every route folder', ...)
with a file comment that says why each direction matters: a registry entry with no folder 404s while still appearing in the sitemap and the footer, and a folder with no entry throws at build time. Under the dynamic route only the second failure was possible, so the first one was invisible until Search Console noticed.
The footer has a smaller version of the same idea. It names seven of the fourteen formats by slug and resolves each one against the registry:
if (!entry) throw new Error(`Footer names unknown test type ${slug}`);
A renamed slug now breaks the build instead of the footer.
What the registry gained
lib/tests/test-types.ts went from 973 lines to 2,228. The new fields are all about the relationship between pages rather than the content of one page:
-
parent. Logical reasoning is now the parent of inductive, deductive, abstract and diagrammatic reasoning. The child links up to the parent under its intro and in its breadcrumb; the parent lists its children before anything else, "so the parent stops competing for the narrower query". Four pages that each want one keyword, and one page that explains the family, beats five pages all trying to rank for "logical reasoning test". -
group. Four display groups on the hub, carrying 4, 5, 3 and 2 formats, with a flag marking which are ability tests rather than judgement or personality, because the cognitive-ability umbrella page links every ability format and nothing outside one. -
coverageNote. Three pages have it, and it exists so a page can say its practice coverage is thin instead of padding.
The fourteen pages reference 382 practice tests between them, 294 distinct out of the 372 we run. Logical reasoning alone lists 78, which is why its HTML weighs 593 KB against the 140 KB of its diagrammatic child. Thirteen of the fourteen keep h1 and <title> character-for-character identical, with a comment saying why: the result snippet and the page heading should match, so nobody wonders whether they clicked the right link.
The rule that decides which tests appear on a page
This is the part that took judgement rather than typing, and the registry's own doc comment is the policy:
A game belongs on a page only if it genuinely practises that format, judged from the game itself and its provider dossier, never from its name. Bridge Abstract is an operator test, so it sits on diagrammatic reasoning; EEI Interpreting Diagrams is map and legend reading, so it does not.
Two tests with "diagram" in the name, one on the page, one off it. Doing that consistently is how /tests/diagrammatic-reasoning ends up with four practice tests when a keyword-matching script would have given it fifteen.
And then the page says so, out loud, in its own FAQ: "Why does this page list only four tests?" The answer names the four vendors whose tests are genuinely diagrammatic and says we do not pad the page with tests that merely have "diagram" in the name.
See it
Open cogniprep.app/tests: fourteen formats in four groups, all served from folders now. Then open cogniprep.app/tests/logical-reasoning and note that the first thing under the intro is a list of its four children, not a wall of practice tests.
Then open cogniprep.app/tests/diagrammatic-reasoning and read the last FAQ. A page admitting its own thin coverage is a strange thing to ship deliberately, and it is the only version of that page we could defend.
Compare the two page sources if you want the size difference: curl -s https://cogniprep.app/tests/logical-reasoning | wc -c against the same for the diagrammatic page.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.