CogniPrep has three families of long-form content pages, and they are all hand-written React:
| family | page files | lines | inline <Link> |
|---|---|---|---|
/employers/* | 212 | 46,778 | 662 |
/blogs/* | 117 | 26,112 | 372 |
/cheating/* | 66 | 10,540 | 61 |
| 395 | 83,430 | 1,095 |
83,430 lines of JSX whose job is prose. Serving all of it is one file, components/shared/content-blocks.tsx, which is 302 lines and exports ten components. They are used 1,035 times across 392 of those 395 pages. The three that use none are the three index pages, which is correct.
A 1,035-to-302 ratio is the easy part of the story. The interesting part is who uses what.
The same vocabulary, three dialects
| block | pages | uses | employers | blogs | cheating |
|---|---|---|---|---|---|
Callout | 247 | 283 | 117 | 91 | 39 |
SpecList | 174 | 197 | 61 | 73 | 40 |
StatStrip | 188 | 188 | 150 | 25 | 13 |
DoDont | 102 | 102 | 22 | 39 | 41 |
CardGrid | 70 | 76 | 6 | 41 | 23 |
NumberedSteps | 69 | 69 | 36 | 24 | 9 |
KeyTakeaways | 43 | 43 | 7 | 32 | 4 |
SplitCompare | 38 | 39 | 4 | 20 | 14 |
SectionIndex | 24 | 24 | 4 | 12 | 8 |
TimeBudget | 14 | 14 | 2 | 10 | 2 |
Nobody specified any of that. StatStrip is an employer-page habit: 150 of its 188 uses, on 71 percent of employer pages and 21 percent of blog articles. KeyTakeaways and CardGrid are blog habits. DoDont leans to the cheating guides, which makes sense once you think about what those pages are for, and which nobody planned.
Two conclusions I did not expect from a census this boring:
A shared component library does not produce a shared style. It produces a shared range, and each family settles into its own corner of it. If you want consistency across families you need a page template, not a block library, and a page template is the thing these pages specifically refuse to be.
The usage profile is a decent description of what a page family is for. Employer pages are reference material, so they lead with figures in a grid. Blog articles are arguments, so they summarise. Cheating guides are advice, so they list what to do and what not to. I could have got that from reading twenty pages; I got it from one Counter.
Why these are JSX and not a registry
The same repository renders several hundred other content pages straight out of data registries, and those pages are much easier to test, lint and count. So why are these 395 hand-written components?
1,095 inline links. A sentence in one of these pages routinely links to two or three other pages mid-clause. The moment prose in a registry needs a link inside a sentence, you have to invent a markup language, write a parser for it, and then write a second function that strips your markup back out when the same string goes into structured data. We have done exactly that elsewhere in this codebase, with inline tokens and a stripInline helper, and it is fine. It is also a thing you only want to own once.
So the split is deliberate: data where the page is a specification, JSX where the page is an argument with links in it. The cost of the JSX side is that a page is 220 lines on average and nothing stops it being 410.
The long tail is not the problem
TimeBudget is used 14 times in 1,029 page files. By the usual reasoning that is a component that should not exist: 1.4 percent uptake, and inlining it would delete a public export.
I think that reasoning is wrong for a content block library, because the cost of a block is not its line count, it is the decision it removes from the writer. The alternative to a TimeBudget used 14 times is 14 pages each inventing a way to show how long a test takes, and that is 14 chances to get the mobile layout wrong. None of these ten blocks is clever. What they are is decided.
The place that reasoning does apply is the other direction. Two components used by employer pages are specific to a single assessment provider, and they live in that provider's own area rather than in the shared file, even though they are used on ten pages each. The test for whether something belongs in the shared file is not how often it is used, it is whether a second family would ever reach for it.
Where this is still soft
There is no test asserting that a new content page uses the vocabulary at all. A page is a React component, so hand-rolling a <div className="grid grid-cols-2 gap-3"> instead of importing StatStrip is a diff nobody will notice, and the only consequence shows up at 320 pixels wide a month later. The block library's real value is the mobile behaviour baked into each one, like the odd-last-tile rule in StatStrip:
// An odd last tile spans both columns of the two column phone grid // instead of sitting beside an empty cell. className="bg-card rounded-lg border p-4 odd:last:col-span-2 md:p-5 md:odd:last:col-span-1" That line is one of about ten similar decisions in the file, and every one of them is a bug somebody already fixed. Reimplementing the grid by hand reintroduces all of them silently.
See it
Two employer guides at the extremes of the page size distribution, both built from the same ten blocks:
- cogniprep.app/employers/eversource is the shortest at 130 lines of source, and uses two blocks: one stat grid and one callout.
- cogniprep.app/employers/australian-defence-force is the longest at 410 lines, and uses four: two callouts, a numbered sequence and a spec list.
Open both and resize to a phone width. The stat grid pairs its tiles two per row and the odd one out spans the full row rather than sitting next to a gap; the spec list drops its label out of a 224-pixel left column and stacks it above the value. Neither page implements any of that, and that is the whole argument for the 302-line file.