CogniPrep's blog is 99 articles, and each one is a
route folder under app/blogs/<slug>/ holding its own TSX page. The metadata
is somewhere else: one registry file of 2,203 lines whose doc comment lists who
reads it.
* Single source of truth for blog post metadata.
*
* Article *content* lives in its own route folder (app/blogs/<slug>/page.tsx) so
* each post can have its own layout. Everything that needs to know about posts
* without rendering them reads from here instead:
*
* - app/blogs/page.tsx (listing cards)
* - app/sitemap.ts (via lib/blog/posts.ts)
* - app/api/indexnow (via lib/blog/posts.ts)
* - app/feed.xml (RSS)
* - components/blog/* (article chrome, related-article cards)
Five consumers, one entry per article, and the entry carries these two fields:
readTime: number;
wordCount: number;
Both are literals. Nothing counts anything.
The read time is the word count divided by 200
Not asserted anywhere, but true across the whole registry. Comparing every
readTime against its own wordCount:
exact matches (readTime === wordCount / 200): 21 of 99
rounded up: 56
rounded down: 20
largest deviation: 1.1 minutes
implied reading speed: 155 to 225 words per minute
So the rule is 200 words a minute, applied by hand, rounded in whichever
direction looked nicer on the day. 900 words became 5 minutes and 1,900 words
became 9. That is fine. 200 wpm is a defensible number for this kind of
material, and nobody is harmed by a minute either way.
The question is what the other number is worth, because the read time is
derived from it and the article is not.
The totals agree. The articles do not
The articles are TSX, so counting their words means counting what they render.
I pulled all 99 pages from production and counted the words inside the one
.blog-content prose column on each:
declared wordCount, all 99 articles: 127,689
rendered prose, all 99 articles: 127,307
0.3 percent apart. If you only looked at the total you would conclude the
numbers are maintained. Per article:
articles whose prose is within 10% of the declared count: 36 of 99
articles that render fewer words than they declare: 70 of 99
median declared: 1,300
median rendered: 1,182
The aggregate is right because the errors cancel. 70 articles are padded, a
smaller group is understated by much larger margins, and the sum lands in the
same place.
Fifteen articles are out by two minutes or more
Taking the same 200 wpm rule and applying it to the words actually on the page,
15 of the 99 advertise a read time two minutes or more away from what they
serve. The worst four in each direction:
| article | advertised | prose words | at 200 wpm |
|---|---|---|---|
| which-assessment-provider-am-i-taking | 5 min | 4,067 | 20 min |
| numerical-reasoning-data-interpretation | 9 min | 2,628 | 13 min |
| inductive-vs-deductive-reasoning-tests | 8 min | 2,459 | 12 min |
| germany-france-and-eu-assessments | 8 min | 2,102 | 11 min |
| us-pre-employment-cognitive-tests | 9 min | 1,180 | 6 min |
| australian-graduate-assessments | 8 min | 1,067 | 5 min |
| civil-service-online-tests-tips | 8 min | 1,097 | 5 min |
| game-based-vs-traditional-assessments | 8 min | 1,182 | 6 min |
The top row is the one worth looking at. It was written as a 900-word
explainer, it is the first entry in the registry so it renders as the featured
card, and it has since grown a comparison table, a section per provider and a
set of worked examples. Its source file is 1,046 lines against a median of 213.
Nobody went back to the registry, because nothing in the build has an opinion
about it.
The same literal is what we tell Google
lastModified is the other hand-maintained field, and it is not cosmetic. It
is what app/sitemap.ts emits:
curl -s https://cogniprep.app/sitemap.xml | grep -A1 "blogs/sova-assessment-tips"
All 99 blog lastmod values in the live sitemap match the registry exactly,
which is the correct behaviour for a field whose job is to be a claim rather
than a file timestamp. The problem is what the claim says: 94 of the 99
articles have a lastModified identical to their original date. Five have
ever been bumped, and the article above, the one that quadrupled in length, is
not one of them.
That is the part I would actually fix first. A read time off by three minutes
costs a reader three minutes. A lastmod that says nothing changed since
September is a claim to a crawler, and the fix is a test rather than a
convention: the repository already has 368 test files, many of them asserting content
invariants that the typechecker cannot see, and "declared word count is within
15 percent of the prose this page renders" is the same shape of assertion as
the ones already in there.
See it
Pick the outlier and the shortest article and compare them yourself. The read
time is rendered next to a clock icon, with React's comment separator between
the number and the unit:
curl -s https://cogniprep.app/blogs/which-assessment-provider-am-i-taking \
| grep -o '[0-9]*<!-- --> min read' | head -1
# 5<!-- --> min read
Then count what the page actually serves, article region only, because the RSC
payload repeats every string further down the document:
curl -s https://cogniprep.app/blogs/which-assessment-provider-am-i-taking \
| python3 -c 'import sys,re,html;h=sys.stdin.read();b=re.search(r"<article>(.*?)</article>",h,re.S).group(1);print(len(re.findall(r"[A-Za-z][A-Za-z-]*",html.unescape(re.sub(r"<[^>]+>"," ",b)))))'
# 4199
4,199 counting the title block and byline, 4,067 counting the prose column
alone. The same two commands on
cogniprep.app/blogs/kpmg-interview-preparation-guide
return 2 minutes and 338 words in the article region, 258 in the prose column, which is the honest end of the distribution.
The full set of 99 is on cogniprep.app/blogs,
where the first card is the featured one and the rest carry no read time at
all.
Top comments (0)