Notifio has fifteen per-site pages, one per rental platform it can watch, at notifio.app/alerts. They come from one React template and a catalogue of typed data. That is the standard programmatic SEO shape, and it is one step away from a doorway farm, so the type file that defines the data opens with a rule:
/** * The shape of an /alerts/[site] page. * * Every field here is prose written for one specific site. The page template in * app/alerts/[site]/page.tsx contributes layout and nothing else: there is no * sentence in it with the site name interpolated into it, because a set of pages * that differ only by a noun is a doorway-page pattern and gets treated as one. * If a field below cannot be filled with something true about this site and this * site alone, the site does not get a page. */ I wrote that comment. I had never checked it. So I fetched all fifteen live pages and diffed their text.
The first pass looks like a pass
Method: fetch each page, strip <script> and <style> blocks, strip the remaining tags, unescape entities, collapse whitespace, split on sentence punctuation, and count how many of the fifteen pages each fragment appears on.
| Appears on | Fragments |
|---|---|
| 1 page | 686 |
| 2 pages | 7 |
| 3 pages | 6 |
| 4 pages | 10 |
| 7 pages | 1 |
| 8 pages | 2 |
| all 15 | 3 |
Three fragments on all fifteen pages, out of 715. The three:
- The entire nav and footer, which has no sentence punctuation in it, so my splitter kept it as one enormous fragment.
- "Mac and Windows, updates included, nothing to cancel."
- "These are the ones people usually pair it with."
One of those is chrome and two are template sentences. 686 of 715 fragments, 96%, appear on exactly one page. I nearly stopped there and wrote a smug post.
The second pass, with the site name taken out
The rule I wrote is not "no repeated sentences". It is "no sentence in the template with the site name interpolated into it". An interpolated sentence is a different string on every page, so a duplicate-text diff cannot see it. That is not a flaw in my script. It is the entire reason the pattern works for the people who do it on purpose.
So I ran it again, this time stripping the footer as well, and replacing each page's own site name with the token SITE before splitting. That pass found 675 fragments on exactly one page, and the picture at the other end changed.
Five more fragments appeared on fourteen of the fifteen pages:
SITE alerts: Sources, so you can check whether SITE has changed this since we wrote it: What to know about monitoring SITE Does it need a login? Most people watch more than one site Notifio runs every monitor at once, so SITE does not have to be the only search you are watching. See every site Notifio can monitor Start watching SITE today One payment, a lifetime licence, and every monitor you can think of. And here they are in the template, which does exactly what the comment says it does not:
Sources, so you can check whether {site.name} has changed this since we wrote it: {`Setting up ${indefiniteArticle(site.name)} ${site.name} monitor`} What to know about monitoring {site.name} {site.name} alerts: common questions Notifio runs every monitor at once, so {site.name} does not have to be the only search you are watching. Start watching {site.name} today Six interpolations, in visible copy, in the file the comment says contributes layout and nothing else.
There is even a helper that exists only because of them:
/** * "an Idealista monitor", not "a Idealista monitor". * * Letter-based rather than phonetic, which is right for every name in the * catalogue: Idealista, ImmobilienScout24 and OpenRent take "an", and none of * the consonant-initial names is one of the exceptions where the sound and the * spelling disagree. */ function indefiniteArticle(name: string) { return /^[aeiou]/i.test(name) ? "an" : "a"; } Two lines of code and eight lines of comment, written to make a templated sentence read naturally. If you ever need evidence that your template is writing prose, look for the grammar helpers. Nobody writes an article-selection function for layout.
Fourteen of fifteen, and the fifteenth is a measurement artefact
Those five fragments appear on fourteen pages, not fifteen, and the missing one is /alerts/huurwoningen every time. The page is not different. Its name field is:
name: "Huurwoningen.nl", My normalisation replaced the site name first and then masked anything that looked like a domain, so "Huurwoningen.nl" became SITE.nl and then DOMAIN, and the five template sentences on that page stopped matching the other fourteen.
I am leaving that in rather than tightening the script, because it is the most useful thing the measurement produced. Text-level dedup is brittle in both directions: one brand that spells its own name with a TLD was enough to hide a page from a check I had deliberately designed to catch it. Any process where "we diff the output" is the control has this failure mode.
So is the rule broken, or is the comment wrong?
Honest answer: the comment is wrong and the rule is fine.
Look at what the six interpolated sentences are. Two headings, one label above a citation list, one call to action, and one cross-sell line. Not one of them is a claim about the platform. Every factual sentence on the page, what that site's own alerts do, whether it needs a login, how its search URLs are shaped, what its paid tier gates, comes out of the per-site data, which is why 675 of the fragments live on exactly one page.
A doorway page is not a page with a templated heading. It is a page whose substance is a noun substitution. By that test these pass, and the number that says so is 675 to 6.
But the comment states something stronger and false, and a false comment is worse than no comment, because the next person to add a sentence to that template will read it as permission to not think. So the fix is to rewrite the rule to say what the template may and may not interpolate:
- May: headings, labels, calls to action, cross-links. Navigational scaffolding that names the page's subject.
- May never: anything describing how that platform behaves, what its alerts do, what it charges, or how to set it up. If a sentence would be a lie about any one of the fifteen sites, it cannot live in the template.
That is a rule you can hold a diff against. "No interpolation" was not, which is presumably why I broke it six times without noticing.
Two other things the diff turned up
The repetition that is deliberate. Every fragment appearing on 2 to 8 pages is a sibling card blurb. Each site lists three related siblings, rendered as cards with the sibling's own twelve-word blurb, so Kamernet's blurb "Rooms, studios and apartments, heavily student. Alerts are free, replying is not." shows up on eight pages: seven siblings that name it, plus its own. That is the internal linking I measured in the internal link post, seen from the text side. The one fragment appearing on exactly seven pages is the word "Yes.", because seven site FAQs happen to open an answer with it.
A claim I missed two days ago. The template's closing call to action says "One payment, a lifetime licence, and every monitor you can think of." I published an audit of every unlimited-searches claim on the site earlier today and counted thirteen pages. That sentence is a fourteenth phrasing, on all fifteen site pages, and my grep for unlimited|no limit|as many went straight past it. The real cap is 15 searches. An audit by keyword finds the phrasings you thought of, which is a weaker guarantee than it reads like, and normalising the template is what surfaced this one.
Go and look
Pick any two of the fifteen, for instance /alerts/kamernet and /alerts/facebook-marketplace, and read them side by side. The headings track each other and nothing underneath them does.
If you would rather measure your own:
for u in https://example.com/a https://example.com/b; do curl -s "$u" | sed 's/<script[^>]*>.*<\/script>//g; s/<[^>]*>/ /g' \ | tr -s ' ' | tr '.' '\n' | sed 's/^ *//' | sort -u > "/tmp/$(basename $u).txt" done comm -12 /tmp/a.txt /tmp/b.txt Then run it again with your subject noun replaced by a placeholder on both sides. The gap between the two runs is the part of your page the template is writing, and if that gap contains a factual claim, the claim is not about the page it is on.


