DEV Community

Daniel Pertu
Daniel Pertu

Posted on

Four pages that name a competitor, 99 links to their own sites, and 17 table rows where the answer is no about us

Nakodo has four pages that name competitors. They are the highest risk content on the site, legally and reputationally, and they are the same data structure as every other page: one typed object in an array, rendered by the same component, checked by the same tests.

This is what we did to make them safe to publish, which turned out to be mostly a writing problem with a small amount of code around it.

The rule is in the repo, not in my head

From docs/seo.md, which is the file you are told to read before adding a page:

Alternatives pages name real companies, so the UK CAP Code's rules on comparisons apply (section 3): fair, verifiable, like with like, no denigration. Every statement about a competitor is attributed and links the competitor's own site, and the page says when it was checked. We never say a competitor lacks something, never quote their prices, and never judge their quality; we say plainly what Nakodo doesn't do. Recheck these pages whenever a competitor changes its site, and bump updated when you do.

I am not a lawyer and this is not advice. The reason that paragraph is in a developer facing document rather than a policy folder is that it has to be in front of whoever adds the fifth page, and the only document that gets read at that moment is the one about how the pages work.

Each page file then opens with the specific version of it:

// Every statement about Modash comes from modash.io and was checked on
// 3 October 2026. Check the linked pages again before changing `updated`.
export const alternative: ContentPage = {
  slug: "modash",
  h1: "Modash alternatives: find creators and have them emailed for you",
  // ...
Enter fullscreen mode Exit fullscreen mode

A comment is not enforcement. What it is: the thing I read before I touch the file, naming the one source of truth and the date it was true.

Every claim is a link to the page it came from

Across the four pages there are 99 outside links and 23 internal ones. The outside links are not references at the bottom; they are inside the sentences, and they are nearly all to the competitor's own site.

The core of each page is a table with three columns. Here is the shape of it, and the rendered version if you want to read the whole thing:

{
  type: "table",
  head: ["Job", "Modash, from its site", "Nakodo"],
  rows: [
    [
      "Find creators",
      "You search a database of public profiles with [AI search, filters and lookalikes](https://www.modash.io/features/influencer-discovery).",
      "Writes a brief from your website and searches its keywords on each platform. No database to browse.",
    ],
    [
      "Pay creators",
      "[Payments](https://www.modash.io/influencer-payments) in creators' local currencies.",
      "No. You agree terms and pay the creator directly.",
    ],
    // ...
  ],
}
Enter fullscreen mode Exit fullscreen mode

The middle column is a quotation with a link, every time. The column heading says "from its site" so a reader knows it is reported rather than tested. And the paragraph above the table says the same thing in words:

{
  type: "p",
  text: "The Modash column comes from the linked pages on Modash's site. The Nakodo column is how Nakodo works today; the full method is on [how Nakodo works](/how-it-works).",
}
Enter fullscreen mode Exit fullscreen mode

"Verifiable" in the CAP Code's sense has an unglamorous practical meaning: a reader must be able to check the claim themselves. A link to the page the sentence came from is the cheapest possible implementation of that, and it has a side effect I did not expect to like as much as I do. When a claim is going to be linked, you cannot write a vague one. "Limited discovery features" has nowhere to point. "You search a database of public profiles" points somewhere.

36 rows, 17 of which say no, all of them about us

Of the 36 comparison rows across the four pages, 17 begin with "No." in the Nakodo column:

"Manage relationships" -> "No. Nakodo steps out after the introduction."
"Codes, gifting and store" -> "No. Nakodo has no store integration."
"Pay creators" -> "No. You agree terms and pay the creator directly."
"Track posts and sales" -> "No. Your own codes and links do that."
Enter fullscreen mode Exit fullscreen mode

Zero rows say "no" about the competitor. That is not politeness, it is the one asymmetry that makes a comparison page defensible: I can state what my own product does not do with certainty, and I cannot state what somebody else's product does not do without having tested every version of it on every plan. So the competitor column only ever contains things their site says they do, and the Nakodo column carries all the negatives.

It also makes the pages better. These are suites that do campaign management, payments, store integrations and tracking, and our product does one job and stops at an introduction. A page pretending that is a feature-for-feature contest would be transparently useless to the person reading it, who is trying to work out which of two different shapes of thing they need.

Which is why every page has these two sections, in this order:

"When Modash is the better choice"
"When Nakodo is the better choice"
Enter fullscreen mode Exit fullscreen mode

Theirs first. The honest test I apply before publishing one of these: if I cannot write the first section with a straight face, the page should not exist, because it means I do not understand what the other product is for.

Each page ends with an "Other options" section linking the other three, so the four form a small cluster rather than four dead ends. Readers comparing tools compare more than two.

What the test suite can and cannot do

These pages are ContentPage entries, so they inherit every check the guides get. The ones that bite hardest here:

function checkLink(href: string, from: string) {
  if (/^https?:\/\//.test(href)) {
    assert.match(href, /^https:\/\//, `${from}: outside links must be https (${href})`);
    return;
  }
  // ... internal links must resolve to a real page
}
Enter fullscreen mode Exit fullscreen mode

Every one of the 99 outside links has to be https. A comparison page that cites a competitor over plain http is a mixed content warning and a bad look, and it is the sort of thing you type once at midnight.

const BANNED = [/\u2014/ /* the em dash, a literal character in the source */, /\bscrap(e|es|ed|ing|er|ers)\b/i, /\bhunt(s|ed|ing|er|ers)?\b/i, /\bsupercharg/i, /\bseamless/i, /\beffortless/i, /game-changing/i, /\bAI-powered\b/i, /unlock the power/i, /!/];
Enter fullscreen mode Exit fullscreen mode

The banned list is a voice rule everywhere on the site, and on these pages it does a second job. "Game-changing", "seamless" and an exclamation mark are the vocabulary of a comparison page that is selling rather than comparing, and a test that rejects them is a test against my own worst instincts at the moment I am writing about a competitor.

Then the ordinary content rules, which still apply: a metaTitle of at most 50 characters (the layout appends " ยท Nakodo" and the rendered title stays under 60), a description between 90 and 158 characters, at least 600 words of body, at least three FAQs, at least two related pages, no duplicate title or description anywhere on the site, and "influencer" allowed in the title and FAQ questions but not in body copy, because it is the searcher's word rather than ours.

And here is what none of that can check: whether any of it is still true.

A test can assert that a link is https and that the page it points at is named in the sentence. It cannot assert that the page still describes the feature it described on 3 October. Nothing automated here protects against a competitor redesigning their pricing page, renaming a feature or dropping one, and the pages themselves say so in the first paragraph of the first section:

text: "This page describes Modash from its own website as of October 2026, and links each statement to the page it came from. Features change, so check those pages before you decide. Nakodo isn't affiliated with Modash. Product names belong to their owners.",
Enter fullscreen mode Exit fullscreen mode

That sentence does three jobs: it dates the claims, it tells the reader to verify, and it disclaims affiliation. The last line is there because a page titled "Modash alternatives" ranking for someone's trademark should never be mistakable for the company's own page.

The thing I would automate, and have not

The obvious missing piece is a job that fetches the 99 linked pages weekly and tells me when one 404s or changes substantially. I have the crawler for it already, because the product reads a brand's own website to write a brief.

I have not built it, and the reason is that the failure mode it catches is not the dangerous one. A dead link is visible and embarrassing. The dangerous case is a page that still loads, still has the same URL, and now describes something different, and a diff on crawled HTML would fire on every marketing tweak until I ignored it. The thing that actually protects these four pages is a recurring task on a human calendar, and updated in the file is the record of the last time it ran.

I will take the diff job when I have more than four of these. At four, the honest answer is that a person rereads them, and the code's contribution is to make it obvious which sentences to check: they are the ones with links in them.

Top comments (0)