Our comparison tables have three states, and 15 of their 46 rows hand the win to the other product
Six comparison pages, five of which carry a feature table, 46 rows between them, and in 15 of those rows the other product gets the tick. In 13 of the 15 we do not have one.
You can count them without reading any code:
curl -s https://pub-trivia.app/compare/paper-vs-app-quiz > p.html
for l in Yes No Partly; do printf "%s: " "$l"; grep -o "aria-label=\"$l\"" p.html | wc -l; done
Yes: 9
No: 6
Partly: 3
Nine rows, eighteen cells, and paper wins four of them. That page is /compare/paper-vs-app-quiz, and the thing it is comparing us against costs nothing and never loses WiFi.
This post is about the two components behind every one of those pages, because a comparison page is not a marketing page with a table on it. It is a document making factual assertions about a product you do not control, published under your own name, which will be wrong at some point in the future whether or not you notice.
Three states, because two states force you to lie
The row type is the whole design:
export type ComparisonRow = {
feature: string
/** What PubTrivia does. */
us: { state: "yes" | "no" | "partial"; detail: string }
/** What the other product does, as checked on the date in the source note. */
them: { state: "yes" | "no" | "partial"; detail: string }
}
Two things in there matter.
The first is partial. A table that can only say yes or no forces every nuance into a tick or a cross, and that is the mechanism by which honest people end up publishing misleading comparison pages. "Does it let the host control the pace" is not a yes or a no for a product built around speed: it is a partial, and the cross you would have put there instead is a claim a reader can disprove in five minutes by opening the other product. Across our six tables, 20 of the 46 rows resolve to partial for the competitor and 9 resolve to partial for us.
The second is that detail is not optional. You cannot express a row as a bare state, because the type will not let you: every cell carries a sentence. That rules out the comparison table's favourite failure mode, which is a column of ticks nobody has to defend. If the honest sentence for a cell is awkward to write, you find that out while writing the row rather than after a reader has written to you.
The three states render as a check, a cross and a minus, each with an aria-label, which is why the counting trick above works at all. The table is legible to a screen reader, and incidentally to curl.
Rows the other product wins
Here is the part that makes the type worth having. These are real rows from the live pages:
- On the Kahoot page, "Question library" is a yes for them and a partial for us, with the detail "An enormous public library, a genuine advantage."
- On the QuizXpress page, they take buzzers and keypads, on-screen branding and very large audiences. Four of seven rows go their way in whole or in part.
- On the AhaSlides page, polls, word clouds and Q&A are a flat no for us.
- On the SpeedQuizzing page, their credits do not expire and ours is the row that says so.
- On the Sporcle Live page, they turn up and host your night for you, which is a different offer from software and is better for some venues.
None of that is generosity. A comparison page whose table is a clean sweep tells the reader one thing reliably, which is that the page was written by the winner. The rows you lose are what makes the rows you win readable.
A citation box that renders nothing when there is nothing to cite
The second component is the source note. Every comparison page ends with one:
export function SourceNote({ checkedOn, sources }) {
if (sources.length === 0) return null
// ...
}
It names the pages the claims were read off, and the month they were read. The competitor links carry rel="nofollow noopener" and target="_blank": we are citing them, not endorsing them, and a comparison page that passes link equity to the product it is comparing against is doing something nobody intended.
The early return is the detail I would defend hardest. Five of the six pages cite at least one page on the competitor's own site, usually their pricing. The paper comparison cites nothing, because paper has no pricing page to have read, and a "Where this comes from" box with an empty list under it is worse than no box at all: it is a credibility signal pointing at nothing. Load that page and the box is simply absent. Load the Kahoot one and it is there with the link in it.
curl -s https://pub-trivia.app/compare/paper-vs-app-quiz | grep -c "Where this comes from" # 0
curl -s https://pub-trivia.app/compare/kahoot-for-pub-quiz | grep -c "Where this comes from" # 1
The date is addressed to whoever maintains the page
checkedOn reads "September 2026" on all six pages at the moment. Its job for a reader is obvious: products change, prices change, so check the current position before deciding anything on the strength of a page written months ago.
Its real job is internal. A comparison page is the one kind of page on a marketing site that decays into being wrong rather than just being stale, because its correctness depends on somebody else's decisions. When that date is a year old, the page needs rechecking before it needs rewriting, and the date is the only thing in the system that will tell you so. It also means the page states its own confidence, which is a cheaper thing to maintain than a page that implies it is current forever.
The six pages are listed at pub-trivia.app/compare, and the product all of them are arguing for is at pub-trivia.app with a free tier that needs no card. If a row on any of those pages is wrong about a product you work on, the source note says to tell us, and that is not decoration either.