Our five sites agree byte for byte about each other, and four of them contradict themselves
Notifio is a desktop app that watches rental search pages and emails you when a new listing appears. It is one of five apps published by the same company, and each one's site carries a file whose entire job is to say the same thing as the other four.
I wrote about that file when there were four sites. There are five now, and I had never actually checked the claim: the file says every site emits an identical publisher graph, and nothing enforces it, because the copies live in five repositories with no shared package between them. So I fetched all five /about pages and diffed what they serve.
The file that is meant to be identical
/** The company that publishes all five apps. */ export const PUBLISHER = { name: 'CogniPrep Ltd', registeredIn: 'England and Wales', companyNumber: '17407266', id: 'https://cogniprep.app/about#cogniprep-ltd', } as const; Then an array of the five apps, each with a key, a name, a URL, a one-line blurb and a paragraph for somebody who arrived from one of the others. One line differs between the copies:
/** The app this repository builds. The one line that differs between copies. */ export const SELF: AppKey = 'notifio'; AppKey is derived from the array, which is why the array is declared with as const satisfies readonly FamilyApp[] rather than annotated:
/** * `as const satisfies` rather than a `: readonly FamilyApp[]` annotation: the * annotation widens every `key` to `string`, which would let a typo in SELF * compile cleanly and list this app as its own sibling. */ That is the one failure mode the type system can catch when you copy a file between repositories. Everything else about the agreement is on trust, including the count in the prose, which is read off the array rather than typed:
const APP_COUNT = COUNT_IN_WORDS[APPS.length] ?? String(APPS.length); So when the fifth app was added, the sentence on the other four sites became "All five are published by CogniPrep Ltd" by itself, in the copies that had been updated.
What the diff says
Five /about pages fetched, every application/ld+json block parsed, the mentions array pulled off each AboutPage node, keys sorted, serialised and compared.
cogniprep.app 5 mentions identical to notifio.app: yes munchable.app 5 mentions identical to notifio.app: yes nakodo.app 5 mentions identical to notifio.app: yes notifio.app 5 mentions identical to notifio.app: yes pub-trivia.app 5 mentions identical to notifio.app: yes Each of the five blurbs has exactly one distinct value across the five sites. The visible sentence under the list matches too, on all five:
All five are published by CogniPrep Ltd, a company registered in England and Wales (company number 17407266). So the copied file is doing its job, which is a duller result than I expected and the one I wanted. The interesting part is everything around it.
The same identifier, two descriptions, one page
Each site emits an Organization node for itself in the root layout, and mentions includes every app in the family, including itself. Both carry the same @id. They do not carry the same description.
Here is https://notifio.app/#organization, twice, on one page:
{ "@type": "Organization", "@id": "https://notifio.app/#organization", "logo": "https://notifio.app/icon.png", "description": "Notifio monitors rental listing sites and sends instant email and desktop alerts the moment new listings appear." } { "@type": "Organization", "@id": "https://notifio.app/#organization", "description": "Watches rental listing sites and alerts you the moment a new listing appears." } Both sentences are true and one of them is going to be thrown away, by a consumer whose merge rules I do not control. An @id is a promise that two nodes are the same thing, so a parser is entitled to take either description, or to take both and treat the property as multi-valued, which is worse: an organisation with two descriptions is not a thing anybody wants in their graph.
I checked all five:
cogniprep.app two descriptions for its own @id munchable.app two descriptions for its own @id nakodo.app two descriptions for its own @id notifio.app two descriptions for its own @id pub-trivia.app one, because its blurb happens to equal its site description PubTrivia passes by coincidence. Somebody wrote the same sentence twice in two files, and the day either is edited it joins the other four.
The fix is one word. The family list exists to tell a crawler that five domains have one publisher, and for that the self entry needs an @id and nothing else: the fuller node is already on the page. Dropping description from the entry that matches SELF leaves four complete sibling nodes and one bare reference, which is exactly what the page means.
What else did not match
The node census per /about page, which nothing in the shared file governs:
site WebSite node BreadcrumbList on /about cogniprep.app no yes munchable.app yes yes nakodo.app yes yes notifio.app no no pub-trivia.app no yes Notifio's own about page is the only one of the five with no breadcrumb markup on it, which I found slightly insulting given it is the repository I was sitting in. Four of the five sites tell a crawler where /about sits; mine makes it look like a root. The site has a breadcrumb component and uses it on every guide page. This page simply never got one.
The company node nobody defines
The publisher @id appears six times on each page and always as the same three keys:
{"@type":"Organization","@id":"https://cogniprep.app/about#cogniprep-ltd","name":"CogniPrep Ltd"} Nothing anywhere expands it. There is no node with a legalName, no identifier carrying the company number, and no element in any page's HTML with id="cogniprep-ltd", so the fragment names an entity rather than a place on a page. That was deliberate and I still think it is right: five sites asserting a shared parent only works if they all assert the identical stub, and the moment one of them starts adding properties the copies are no longer copies.
The part I am less comfortable with is that the company number is in a sentence a person reads and in nothing a machine reads. It is public information, printed on the page, on all five. One site should define the node properly, and the obvious candidate is the one the @id points at.
The thing worth copying from this
If you keep one file in sync across repositories by hand, the failure you expect is drift in the file, and that is the failure that did not happen. What happened instead is drift in everything the file does not cover, plus one bug caused by the file being correct: the self entry is only a contradiction because the array is complete, and a complete array was the whole point.
Both halves are visible from outside, which is the part I would push on. Five curl calls and a JSON parse told me more about this than reading the file did, and the file is the thing I wrote.
Notifio itself is at notifio.app with the family list on notifio.app/about, the per-site setup pages at notifio.app/alerts, the comparisons at notifio.app/compare, and a one-time licence with Mac and Windows installers.