· Blog
We audit how websites present to AI systems. On September 16, 2026, we ran the same checks against our own site, generativemetrics.com, and found six gaps. Each one was small, specific, and invisible from a browser. We fixed them in two commits (d1051bc and 6b7669) and re-ran the checks.
There are two reasons to write this down. First, the failures are a sample of what any site can get wrong between a browser and a machine. Second, every item is verifiable: each fix is a named file and a specific change. Nothing below is a measurement of AI behavior — it is a record of what our pages expose and what we changed.
The homepage title had lost its brand suffix
Next.js applies a layout title template to child segments, not to the segment where the template is defined. The homepage shares the root segment, so when the root default title changed, the homepage title dropped its brand suffix with no error anywhere. We restored the suffix as a hardcoded string in app/page.tsx (commit 6b7669, one file). The root page is the one place a title template does not reach, and it needs the brand written out.
The sitemap shipped stale dates
Every URL in our sitemap carried a hardcoded last-modified date; most shared one stale value, and none were tied to the source file that changed. Anything reading it was told the site had barely changed, while releases kept shipping. The dates are now derived per route from the git history of each route source file (app/sitemap.ts), with the source file noted next to each entry so the date can be re-derived. A stale lastmod is a machine-readable statement that happens to be false; git history is a better source for it.
Our Organization data was a stub
Structured data is how a site describes itself to machines. Ours held two fields: name and url. We enriched it with logo, description, contactPoint, and alternateName (app/layout.tsx).
We deliberately did not add sameAs links. sameAs is where a site lists its official profiles elsewhere, and we do not have any real public profiles yet. Filling that field with URLs that do not exist would be a fabricated identity signal, and fabrication is not a thing we are willing to ship. Leaving it empty until there are real profiles to point to is the honest state.
robots.txt blocked our own probe endpoint
Our robots.txt disallows /api by default, which is right for private endpoints. But the rule was blanket, and it also covered /api/public-probe — the unauthenticated quick-check endpoint that exists for agents. An agent that followed our published policy was told to stay away from the one endpoint meant for it. We added a targeted Allow: /api/public-probe while keeping the rest of /api disallowed (app/robots.ts).
The probe was live but undiscoverable
Allowing a path in robots.txt only helps if an agent knows the path exists. /api/public-probe had shipped without documentation. We documented it in llms.txt and on the API docs page, including the JSON body shape and the per-IP limits. Access and discoverability are separate checks, and we had failed the second one.
Plain definitions on the pages a machine reads first
The homepage, pricing, and FAQ now each carry a roughly 60-word definition block: what the service is, what it records, what the results are, and where the limits are. The purpose is extraction — a machine should be able to pull a first-pass explanation from the page itself instead of assembling one from marketing copy. The blocks say the same thing the rest of the site says, limits included.
Update, September 20, 2026: two before/after pairs from our own rescans
Every section above describes changes to our site. This one is the opposite, and it was added later. On September 19 and 20, 2026 we ran the same checks against generativemetrics.com again, and the runs that failed were not telling us about our website. They were telling us about our scanner and, in the second pair, about the edge in front of the site. Nothing about generativemetrics.com changed between any of the four scans below. What changed was the tool doing the measuring, and the infrastructure in front of the site. We are publishing both rows of each pair, failing run and passing run, because a before/after that shows only the “after” is not evidence. Each run is a sealed bundle with its own SHA-256; every row below carries the scan identifier, timestamp, protocol set, verdict and digest needed to tell it apart from the others, and a bundle can be checked against the digest printed next to it — a mismatch is reported as an integrity failure rather than served.
Pair 1 — our scanner read a bot challenge as our homepage
Before — scan c9677eb6-20dd-4a8e-998c-3fff00889f74, 2026-09-19T10:11:06Z, protocol set gm-ai-consumer-compat/1.2.0, verdict FAIL / COMPLETE, sealed bundle SHA-256 5d39852b3a3e56f1eed944cde18dff5badbe1911a5989b0e55a9844fbf21b752. Findings: BLOCKER HTTP_ACCESS_DENIED, BLOCKER RAW_MAIN_CONTENT_DEFICIT, BLOCKER RAW_STRUCTURED_DATA_DEFICIT, WARNING STRUCTURED_DATA_VISIBLE_TEXT_MISMATCH.
After — scan 7c40b261-125a-4fb4-9db8-5854daab0528, 2026-09-19T13:37:04Z, protocol set gm-ai-consumer-compat/1.3.0, verdict PASS / COMPLETE, sealed bundle SHA-256 12fc6e702ab33d5e906c86b87c1872ea03c24a6b75a9ef9679b398dfe7b62e5a. Findings: WARNING STRUCTURED_DATA_VISIBLE_TEXT_MISMATCH only.
In the first run, the request for our homepage was answered with a host bot-challenge: an access-control response body, not the page. The audit read that checkpoint document as the homepage and compared it against the browser-rendered page. A checkpoint has a title, a short visible body, and no structured data — which is exactly the shape the content checks are looking for. The HTML the fetcher received looked like a page that had lost its main content and its structured data, and the report said so, in three blockers, with protocol evidence attached. Two of those blockers were statements about a challenge page. The third reported a single denied request as a property of the site.
That is what a false positive looks like here: not a crash, and not an obviously broken number. A bot-challenge body can masquerade as a page because it is a page — just not the one that was requested. It has a title, some visible text, and none of the markup a real page carries, so every comparison against the rendered page comes out as a deficit. The finding is well-formed, cites real observations, and is indistinguishable on screen from a genuine one. Nothing about it looks wrong; the only thing wrong is which document was measured.
Why the fix was structural, not a vendor special case
Release e23b8df (September 19, 2026) put an acquisition-outcome gate in front of every content check (lib/evidence/acquisitionOutcome.ts). It asks one question before anything else runs: did the audit actually obtain the requested representation? The answer is derived from the HTTP status alone. If the response was not acquired, the content and structured-data protocols abstain instead of judging it — they cannot report a content deficit for a document that was never the page. Protocol set gm-ai-consumer-compat/1.3.0 sealed that behaviour.
We deliberately did not write this as a check for the challenge markup of the host that blocked us. A signature list is a vendor special case that covers only the vendors someone thought of, breaks when a checkpoint is restyled, and never generalizes to the next origin, CDN, or WAF. HTTP status is different: it is the protocol's own vocabulary, it is stable, and it means the same thing everywhere. So the gate reads 401, 402, 403, 407 and 451 as access denied, 429 as throttled, 404 and 410 as not found, 5xx as a server error, 3xx as an unresolved redirect, and anything else as unreachable — the same way for any origin.
The trade-off, stated plainly: the gate does not try to decide whether a body served with a 200 is a page or an interstitial. A challenge that arrives with a 200 is handled elsewhere, by comparing the non-browser capture with the browser's own navigation, and where that comparison is ambiguous the outcome is INCOMPLETE. A legitimate page that leads with challenge-like text can therefore be reported as INCOMPLETE. That is a deliberate one-sided bound — the alternative is guessing, and a guess in either direction is worse than an abstention.
At 2026-09-19T13:37:04Z the same checks ran under protocol set 1.3.0 and completed with the page acquired: PASS / COMPLETE, no blockers, the same single WARNING. Nothing about generativemetrics.com was changed to produce that. The difference between the two rows is the scanner.
Two things we are not saying. The remaining STRUCTURED_DATA_VISIBLE_TEXT_MISMATCH WARNING is present in both rows above: the fix did not resolve it, and it is not presented here as resolved. And this pair is not a claim that our site got better between 10:11 and 13:37 UTC — no content, markup, or policy on generativemetrics.com changed on September 19.
Pair 2 — our own edge was blocking our own scanner
Before — scan 9f6488d4-0d01-4856-aef5-754626312fbf, 2026-09-20T09:17:13Z, protocol set gm-ai-consumer-compat/1.3.1, verdict INCOMPLETE / PARTIAL, sealed bundle SHA-256 98f0f9678c7859757a78699b4bd5300f105f38e0399da1d1748179f1df66778c. Findings: INFO HTTP_ACCESS_DENIED, INFO ACQUISITION_NOT_ACQUIRED, INFO ACTOR_ACCESS_DENIED ×5.
After — scan 84988811-b566-40f8-95aa-aca4f0f81870, 2026-09-20T10:44:46Z, protocol set gm-ai-consumer-compat/1.3.1, verdict PASS / COMPLETE, sealed bundle SHA-256 63dd817fdc579c3032ad0dff56e0352d6852e0bbfb8d09ee7d7c8e77b0906364. Findings: WARNING STRUCTURED_DATA_VISIBLE_TEXT_MISMATCH only.
The first run is the gate from the previous section doing its job while the underlying cause was still in place. Our site had no allowlist for our own scanner at the edge, so requests from the scanner were challenged. The audit did not turn that into content findings. It recorded that the page was not acquired, reported the denial at INFO severity, and returned INCOMPLETE — the honest answer for a page the scanner was never served. This is the same situation that produced the fabricated blockers in pair 1, and the fix is why it now produces an abstention instead.
The fix for this pair was infrastructure, not content. We fronted the site with Cloudflare and added a skip rule that lets the scanner through (recorded as ledger item D24 in our internal roadmap). Not one line of page content, markup, or policy changed. The 10:44:46Z rescan then acquired the page and completed. Read the two pairs together and they point the same way: in neither case did the website change, and in both cases the record did. We were blocking our own audit, and the audit said so on data we then had to publish.
What this proves, and what it does not
The narrow claim is that the rescan-verification loop works on real data: four sealed runs, all of them against the scanner's own operator, verdicts before and after, and a digest per run so a reader can tell one row from the other without trusting our prose. The second claim is less comfortable and more useful. The tool can fail against the company that runs it, in public, and find its own defects before a customer does. In pair 1 the failure was in our scanner and would have reached a customer as fabricated blockers. In pair 2 the failure was our own edge refusing our own audit. Neither was found by a customer complaint; both were found by re-running the check and reading the result. That is the point of publishing receipts rather than screenshots of successes.
It does not prove the scanner is now free of false positives — the WARNING present in both rows of pair 1 is an open item, not a fixed one. It does not prove anything about another site, and it does not measure what any AI system does with this content. The paragraphs that follow are unchanged by any of it.
What this exercise does and does not prove
The checks above are the ones the product automates: what each named AI crawler is permitted to do against your declared policy, what machine-readable files declare, what structured data says, and what a fetcher receives in raw HTML without running a browser. Running them on our own site produced a short, concrete list, which is the point — a finding you can name is a finding you can fix.
It does not prove that any AI system will cite, rank, or recommend this site. It does not prove the product works for your site; one site, reviewed once, is not evidence about any other site. It does not prove the fixes are permanent — any of them can be undone by a later change, which is why the sitemap dates come from git and why the checks can be re-run. And it does not measure what any model will do with the content it can reach.
What it does prove is narrower and verifiable: the gaps were found, the changes shipped, and the files are in the repository history. If you review how your own site presents to AI systems, that is the standard to hold the review to — named findings, named fixes, and no claims beyond what was observed.
Run the same checks on your own site.