Notifio is a £20 desktop app that watches rental search pages and alerts you when a new listing appears. It competes with several subscription services, so the site has a page per competitor at notifio.app/compare.
Pages like that are usually worthless, and everybody reading them knows why: they are written by the people who lose if you pick the other product. The interesting question is whether you can make that structurally harder, in code, rather than promising yourself you will be fair.
Here is the type those pages are built from:
export type CompareTarget = {
slug: string;
name: string;
domain: string;
...
plans: ComparePlan[];
rows: CompareRow[];
whereTheyWin: { heading: string; body: string[] };
whereWeWin: { heading: string; body: string[] };
faqs: { q: string; a: string }[];
sources: CompareSource[];
related: string[];
};
whereTheyWin has no question mark. A new comparison page does not compile until somebody has written at least one string explaining why you might buy the other thing.
Why that is a type and not a guideline
The comment at the top of the directory states both rules the files obey:
/**
* First, every price is quoted from the other company's own pricing page and
* carries a dated source link on the page. Subscription prices change; a
* comparison page that cannot be checked is a liability rather than an asset.
*
* Second, `whereTheyWin` is not optional and is not a straw man. Notifio runs on
* the user's own machine, which is a genuine disadvantage against a cloud
* service if that machine is a laptop that gets closed at night. A comparison
* page that hides that loses the reader the first time they think about it, and
* pages that concede a real point outrank pages that do not.
*/
An optional field for the counter-argument is a field that gets filled in on the first page and left empty on the fourth, because by then you are shipping and the section feels like homework. A required field means the compiler asks the question at the moment you are writing, which is the only moment you are actually thinking about that competitor.
This is the cheapest content governance mechanism I know of. The type is not enforcing quality, and it cannot tell a real concession from a weak one. What it enforces is that the question was asked every single time, and in practice that is most of the distance.
Content rules in a type system work for anything structured: a sources array that cannot be omitted, a pricingNote that forces somebody to write down what the plan table does not say out loud, a dated price string. I wrote about the same idea applied to the per-site landing pages in fifteen required fields and no optional ones.
The template renders their case first
{/* ── Their case, first ── */}
<Prose heading={target.whereTheyWin.heading} body={target.whereTheyWin.body} />
<Prose heading={target.whereWeWin.heading} body={target.whereWeWin.body} />
Two lines, in that order, for a reason spelled out at the top of the route:
/**
* The template renders `whereTheyWin` above `whereWeWin` on purpose. A reader
* who arrived searching "<product> alternative" is already sceptical of whoever
* wrote the page, and the fastest way to be trusted on the second claim is to
* concede the first one before they have to argue for it.
*/
Because the order lives in the template rather than in the data, no individual page can quietly decide to lead with its own case. You can check it on any of them: notifio.app/compare/rentbird opens its prose with "Where Rentbird is the better buy".
The table has a verdict column, and it does not always point at us
export type CompareRow = {
dimension: string;
them: string;
us: string;
/** Set when the honest answer favours them, so the table can show it. */
advantage?: "them" | "us" | "even";
};
The renderer passes that straight to the cells:
<Cell label={target.name} value={row.them} won={row.advantage === "them"} />
<Cell label="Notifio" value={row.us} won={row.advantage === "us"} />
The winning side is rendered brighter. Here is the current count across all five pages:
$ grep -o 'advantage: "[a-z]*"' src/lib/compare/targets.ts | sort | uniq -c
2 advantage: "even"
13 advantage: "them"
34 advantage: "us"
49 rows, and although the field is optional, every row has one. Thirteen of them hand the row to the competitor, and they are not decorative rows either:
Runs when your computer is off | Yes, it is a cloud service | No, your machine must be on and awake
Setup effort | Describe what you want | You paste one search URL per site
Mobile notifications | App and email | Email, plus a desktop notification
Breadth | 1,000+ sites scanned | Only the searches you add
WhatsApp alerts | Yes | No, email and desktop notification
General-purpose use | Watches anything on the web| Rental listings only
The first of those is the product's central weakness. Notifio monitors from your own machine, so a closed laptop monitors nothing, and a cloud subscription does not have that problem. Four of the five pages carry that row and all four give it to the other product. Hiding it would not make it less true, it would make the rest of the table unreliable, and a reader who finds one thing you concealed stops reading the parts you were honest about.
Under the table there is a caption, in the smallest grey text on the page:
Brighter text marks whichever side we think genuinely wins that row, including the
rows where it is not us.
That sentence is the one bit of this I would defend hardest. A highlighted comparison table with no explanation of the highlighting is a visual claim a reader cannot audit. Telling them what the brightness means, and that it sometimes goes the other way, turns it from decoration into information.
Every competitor price is a quoted string
export type ComparePlan = {
/** e.g. "1 month" */
label: string;
/** Exactly as published, currency symbol included. */
price: string;
note?: string;
};
A string, not a number, because the published figure is the artefact. "€16.33 / month" with a note saying "€49 up front, their most popular" is what the other company's page says, and reformatting it into our own currency machinery would turn a quotation into a calculation of ours.
The page then dates it:
Prices read from {target.name}’s own pages in {PRICES_CHECKED} and quoted as
published: <a href={source.url} rel="nofollow noopener" target="_blank">...</a>.
Check them before you buy either product, because theirs can change, and ours is the
one number on this page we control.
PRICES_CHECKED is one exported constant, "September 2026", rendered on all five pages. One edit updates the date everywhere, which sounds convenient and is actually the hazard: the constant makes it trivially easy to claim a re-check you did not do. The mitigation is that the links are right there next to it, outbound to the competitor's own pricing page, so the claim is checkable by any reader in one click.
Those links carry rel="nofollow noopener", which is worth being upfront about rather than quiet about: we link to the source so the price can be verified, and we do not pass ranking signal to a competitor while doing it. That is the normal use of the attribute, and the reader gets the same verification either way.
The last clause of that caption is the part I like: "ours is the one number on this page we control". A comparison page where the author's own price is the only unverifiable figure would be a strange artefact. It points at notifio.app/pricing, which is checkable too.
The one place this leaks into infrastructure
The sitemap gives these pages a change frequency their edit rate does not justify:
/**
* Comparison pages quote other companies' prices, so they carry a higher
* change frequency than their edit rate suggests: a stale price on one of
* those is worse than a stale sentence anywhere else on the site.
*/
A wrong sentence about our own product is embarrassing. A wrong price about somebody else's product is a different category of wrong, because they did not consent to being quoted and cannot correct it. Asking crawlers to come back more often is a small hedge against the window in which a page is wrong, and the real hedge is the dated source links.
What this costs
Being honest in a required field is not free, and I would rather name the costs than pretend the design is pure upside.
It costs sales. Somebody reading "no, your machine must be on and awake" and closing the tab is a person who correctly identified that they wanted a cloud service. That is the system working, and it still shows up as a bounce.
It costs maintenance. Five pages of quoted prices are five pages that go stale on somebody else's schedule, and the only defence is going back and reading their pricing pages again.
And it constrains how the product can be positioned. A page that concedes cloud uptime cannot also imply the desktop model is better in every way, which means the rest of the copy has to do actual work: the arithmetic after month one, searches an aggregated crawl cannot reach, and nothing about your search leaving your own machine.
The pages are at notifio.app/compare, the app is at notifio.app/download, and if you only read one of them, read the row about the closed laptop.
Top comments (0)