Our comparison pages tell the reader September and the crawler October, and both dates are true
We have seven comparison pages: five named after a competitor, one that compares five of them at once, and one that compares an app to pen and paper. Every one of them makes assertions about somebody else's product, and
We have seven comparison pages: five named after a competitor, one that compares five of them at once, and one that compares an app to pen and paper. Every one of them makes assertions about somebody else's product, and assertions about somebody else's product go out of date on their schedule rather than ours.
So each page carries a box at the bottom naming the pages the claims were read off, and the month they were read. The component that renders it starts with its own reason for existing:
/**
* Where a comparison page's claims about another product came from, and when.
*
* This is not decoration and it is not a disclaimer bolted on at the end. A
* comparison page makes assertions about somebody else's business, and those
* assertions go out of date: a competitor changes their pricing and the page
* that says otherwise is wrong, publicly, with their name on it.
*/
Twelve citations across five competitor domains, and you can count them from outside:
$ curl -s https://pub-trivia.app/compare/best-pub-quiz-apps | grep -o 'rel="nofollow noopener"' | wc -l
5
$ curl -s https://pub-trivia.app/compare/quizxpress-alternative | grep -o 'rel="nofollow noopener"' | wc -l
2
Three to Sporcle, three to QuizXpress, two each to AhaSlides, SpeedQuizzing and Kahoot. All of them rel="nofollow noopener" with target="_blank", which is the one decision in here I would not argue about: a page that cites a competitor's pricing page should send the reader there to check, and should not be passing them ranking signal while it does.
The page that cites nothing shows no date
One of the seven renders no box at all. The paper comparison has nobody's pages to have read, so it passes an empty array, and the component bails:
// Nothing to cite means nothing to say. The paper comparison has no
// competitor's pages to have read, and a "where this comes from" box with an
// empty list under it would be worse than no box.
if (sources.length === 0) return null
Which is right about the box and has a consequence I had not considered. The date lives inside the box. So the paper comparison is the one comparison page that shows the reader no freshness information whatsoever:
$ curl -s https://pub-trivia.app/compare/paper-vs-app-quiz | grep -c 'Where this comes from'
0
It still passes checkedOn={"September 2026"} in the source, where nothing reads it. Pen and paper has not changed its pricing since the Biro, so nothing is actually wrong on that page, but the mechanism is: a claim's date is only shown when the claim has a URL attached.
Two dates, two audiences
Every comparison page carries a second date that the reader never sees, in the sitemap:
$ curl -s https://pub-trivia.app/sitemap.xml | tr -d '\n' \
| grep -o '<loc>[^<]*/compare[^<]*</loc><lastmod>[^<]*' \
| sed 's/<loc>//;s|</loc><lastmod>| |;s/T.*//'
https://pub-trivia.app/compare 2026-09-14
https://pub-trivia.app/compare/best-pub-quiz-apps 2026-10-15
https://pub-trivia.app/compare/speedquizzing-alternative 2026-10-15
https://pub-trivia.app/compare/sporcle-live-alternative 2026-10-15
https://pub-trivia.app/compare/quizxpress-alternative 2026-09-14
https://pub-trivia.app/compare/kahoot-for-pub-quiz 2026-10-15
https://pub-trivia.app/compare/ahaslides-alternative 2026-10-15
https://pub-trivia.app/compare/paper-vs-app-quiz 2026-09-14
Five of the seven spokes tell a crawler the page changed on 15 October. All seven tell a reader the competitor's pages were read in September. Both are true, and they are about different things. lastmod comes from each page's node in our content registry, which records the date the page's own content changed, and checkedOn records the date somebody else's content was last looked at.
What changed on 15 October is the part worth knowing:
$ git log --name-only --date=short --pretty='%ad %s' -- app/compare/ | head -8
2026-10-15 Update site copy from five to thirteen question packs
app/compare/ahaslides-alternative/page.tsx
app/compare/best-pub-quiz-apps/page.tsx
app/compare/kahoot-for-pub-quiz/page.tsx
app/compare/speedquizzing-alternative/page.tsx
app/compare/sporcle-live-alternative/page.tsx
A sentence about how many question packs we ship. Our own product, our own copy, five files, and the two pages that did not mention the number kept their September date. Nothing about any competitor was rechecked that day, and nothing in the pipeline suggested it should be.
That is the gap. The signal the sitemap sends is "this page is fresher than you thought", the signal the page sends is "the facts in it are six weeks old", and the first one is the one a search engine reads. I do not think the dates are wrong. I think there are two freshness facts on these pages and only one of them has a mechanism behind it.
While I was in there: the table census
The comparison itself is a three-state table, and the third state is the point:
/**
* A two-column feature comparison.
*
* Three states rather than two, and the third is the important one. A table
* that can only say yes or no forces every nuance into a tick or a cross, and
* that is how comparison pages end up misrepresenting the other product.
* "Partial" is very often the honest answer, and it comes with a sentence
* explaining itself.
*
* Every row's `them` value has to be something checked against a page we link
* to, not something assumed.
*/
Six of the pages use it, for 46 rows in total. Counting the states:
| Column | Yes | Partly | No |
|---|---|---|---|
| PubTrivia | 31 | 9 | 6 |
| The other product | 15 | 20 | 11 |
So we take a cross on 6 of 46 rows and a hedge on 9 more, and the competitor's column is mostly "partly", which is what the component was built for. The states are in the markup as accessible labels, so the ratio is checkable per page without reading a word:
$ curl -s https://pub-trivia.app/compare/quizxpress-alternative \
| grep -o 'aria-label="\(Yes\|No\|Partly\)"' | sort | uniq -c
2 aria-label="No"
5 aria-label="Partly"
7 aria-label="Yes"
That page is the interesting one. Across its seven rows, the QuizXpress comparison gives them four ticks and three hedges with no crosses at all, and gives us three ticks, two hedges and two crosses. On our own comparison page, on the table we wrote, they score better than we do. It is a mature desktop product with fifteen years of features, we are a phone and a QR code, and a table that pretended otherwise would be the fastest way to lose a reader who has used both.
What I would change
Nothing in the code couples the two dates, and nothing ages either of them:
-
checkedOnis a string passed to a component, not a field on the page's registry node, so the date a competitor was last checked is the only page fact that does not live with the other page facts. - No test fails when a citation date gets old. The audit is "somebody notices".
- A change to our own copy bumps
lastmodand silently widens the gap between the two dates, which is exactly what happened on 15 October.
The fix I want is small. Move checkedOn into the registry node next to updated, so the two dates sit side by side in the one file somebody reads when they touch a page, and add a test that fails when any comparison page's checked date is more than a few months behind the build date. That turns the oldest claim on the site into a red test instead of a thing I happened to find while writing a blog post.
The recheck itself is now on the list, which is the one honest conclusion available here: writing this up is what dated the pages, not any process we had. If you want to see the pages, the comparison hub links all seven, the five-way roundup carries the most citations, and every one of those citations goes to the competitor's own page so you can check our claims against theirs rather than taking ours.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.