Dev.to WebDev 🛠 Dev 👁 0 📖 7 min read

49 rows in our comparison tables, 13 of them say the competitor wins, and the marker is a hex colour

Notifio has five comparison pages, one per competitor: Rentbird, RentSlam, Stekkies, Visualping and Rentola. I wrote about these pages recently from the angle of their two dates, the month we read the competitor's pricin

Notifio has five comparison pages, one per competitor: Rentbird, RentSlam, Stekkies, Visualping and Rentola. I wrote about these pages recently from the angle of their two dates, the month we read the competitor's pricing page against the month our sitemap says the page changed. This one is about the tables themselves, and specifically about who the tables say wins.

The type makes conceding mandatory

Each page is one object. Two fields on that type are not optional, and the doc comment says why:

/**
 * The shape of a /compare/[slug] page.
 *
 * Two rules govern everything in this directory.
 *
 * 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.
 */

The table row type carries the same idea at a finer grain:

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";
};

It is optional in the type, which turns out not to matter: every row across all five pages sets it. Counting:

$ cd server/src/lib/compare
$ grep -c "dimension:" targets.ts
49
$ for a in them us even; do printf "%-5s %s\n" "$a" "$(grep -c "advantage: \"$a\"" targets.ts)"; done
them  13
us    34
even  2
Page Rows Us Them Even
Rentbird 10 6 3 1
RentSlam 10 7 3 0
Stekkies 10 7 3 0
Visualping 11 9 2 0
Rentola 8 5 2 1

Thirteen rows out of forty-nine, on our own pages, in tables we wrote, awarded to the other product. That ratio is not generous and I would not pretend it is: a page that handed over half the rows would be a page arguing against buying the thing. What it is not is zero, and zero is what these pages look like almost everywhere else on the web.

The row we lose four times out of five

The thirteen are not spread evenly. One dimension appears on four of the five pages and is conceded on all four:

{ dimension: "Runs when your computer is off",
  them: "Yes, it is a cloud service",
  us: "No, your machine must be on and awake",
  advantage: "them" },

Notifio is a desktop app that polls search pages from the user's own machine. That is the reason your searches never leave your computer, and it is also the reason it stops watching when you shut the lid. Every cloud competitor beats it on exactly that, every time, and there is no wording that makes it not true. The fifth page does not concede it only because Rentola is a listings portal rather than an alert service, so the row does not apply.

The prose section underneath says it at length rather than leaving it as a table cell:

"It runs in the cloud, and that is not a small thing. Rentbird's bots do not care
whether your laptop is shut, out of battery, or in a bag on a train. Notifio only
monitors while the machine it is installed on is awake and online, so if you do
not have a desktop or a laptop you are willing to leave running, Rentbird will
catch listings that Notifio sleeps through. That is the single most important
difference between the two products and it is the one we lose."

The other conceded rows are the ones you would expect once you accept the first: setup effort, because a cloud service asks you to describe what you want and we ask you to paste a search URL per site; mobile notifications, because a phone app beats an email; WhatsApp alerts on the one competitor that has them; breadth on the aggregator that crawls more sites than any one person will paste in; general-purpose use on Visualping, which monitors any page on the web and not just rentals; and on Rentola, the two rows about owning the listings and holding the landlord's contact details, which a portal does and a monitor cannot.

The verdict paragraph at the top of each page is written to be usable by someone who should buy the other thing:

verdict:
  "If you want something that works while your laptop is shut and you never want
  to paste a URL, pay Rentbird. If you would rather own the tool, choose the exact
  searches yourself, include sites no aggregator can reach, and stop paying after
  the first month, Notifio costs less in month one than Rentbird does and never
  charges again."

The entire win marker is a hex colour

Here is the part I am not happy with. The table renders each row as three columns, and the advantage flag becomes one boolean per cell:

<p className="text-[13px] font-semibold text-[#fafafa]">{row.dimension}</p>
<Cell label={target.name} value={row.them} won={row.advantage === "them"} />
<Cell label="Notifio" value={row.us} won={row.advantage === "us"} />

And Cell spends that boolean on a text colour:

function Cell({ label, value, won }: { label: string; value: string; won: boolean }) {
  return (
    <p className={`text-[13px] leading-relaxed ${won ? "text-[#e5e5e5]" : "text-[#737373]"}`}>

#e5e5e5 for the winner and #737373 for the loser, on a near-black background. There is no icon, no label, no aria-*, nothing in the text itself. The page explains the convention in a caption under the table, which is honest and does not help:

<p className="mt-4 text-[12px] leading-relaxed text-[#3a3a3a]">
  Brighter text marks whichever side we think genuinely wins that row, including the
  rows where it is not us.
</p>

So the single most carefully considered piece of information on these pages, the verdict per row, reaches a screen reader as nothing at all. It also reaches anyone who cannot distinguish two greys as nothing at all. An "even" row renders as two dim cells, which is distinguishable from a won row only by the absence of brightness somewhere else in the same row.

The flip side is that it is trivially countable from outside, which is how I checked that the source counts above match what is actually served:

$ for t in rentbird rentslam stekkies visualping rentola; do
    printf "%-11s %s\n" "$t" "$(curl -s https://notifio.app/compare/$t \
      | grep -o 'leading-relaxed text-\[#e5e5e5\]' | wc -l)"
  done
rentbird    18
rentslam    20
stekkies    20
visualping  22
rentola     14

Halve each of those, because the RSC flight payload repeats the markup: 9, 10, 10, 11, 7. That is 47, which is 49 rows minus the 2 marked even, and it splits 34 to 13 exactly as the source says. The numbers agree, which is the only reassuring thing in this section.

Every price carries a link

The other required field is the citation list, and the reason it exists is that these pages quote other companies' money:

export const PRICES_CHECKED = "September 2026";

Rentbird's row of plans, for example, is four entries read off their own pricing page, with the awkward ones kept in rather than trimmed:

plans: [
  { label: "1 month", price: "€29", note: "billed monthly" },
  { label: "2 months", price: "€19.50 / month", note: "€39 up front" },
  { label: "3 months", price: "€16.33 / month", note: "€49 up front, their most popular" },
  { label: "Rentbird Plus", price: "€1,495 + €299", note: "concierge service, agents apply for you" },
],

And the note under the plans says the things a comparison page is tempted to leave out:

pricingNote:
  "Rentbird's plans are prepaid blocks rather than a rolling monthly bill, and it
  advertises a full refund if you are not happy. The subscription covers up to five
  searches and includes AI-generated response templates and one search-buddy
  invitation.",

A refund policy and a feature list, on a page whose job is to sell against them. That is in there because the alternative is a reader who checks and finds out we left it out, and at that point every other number on the page is suspect too.

The sitemap treats these pages as more volatile than their edit rate suggests, for the same reason:

/**
 * 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.
 */

What I would change

The accessibility gap is the only real bug here, and it is a small fix. The advantage flag should produce text, not just a colour: a visually hidden "wins this row" inside the winning cell would carry the verdict to a screen reader and cost nothing visually. A visible marker on the winning cell would be better still, since it would also serve anyone reading the table on a phone in daylight, where the difference between #e5e5e5 and #737373 is considerably less obvious than it is on my monitor.

The second thing is a test. advantage is optional in the type and set on all forty-nine rows, which means the day somebody adds a row and forgets it, the row renders with both cells dim and reads as "even" without anybody deciding that. Making it required is one character and would have been the right call from the start.

If you want to see how the concessions read in place, the Rentbird comparison gives away three rows including the one that matters most, the Rentola comparison is the only one that does not concede the lid-shut row, and the comparison hub links all five. Every price on them links to the page it was read from, so the fastest way to find out whether this post is still true is to check one.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.