Skip to content

Structured data on every route, and a dateModified derived from git - #76

Merged
robbyrob42 merged 2 commits into
primaryfrom
rob/geo-foundations
Sep 4, 2026
Merged

Structured data on every route, and a dateModified derived from git#76
robbyrob42 merged 2 commits into
primaryfrom
rob/geo-foundations

Conversation

@robbyrob42

Copy link
Copy Markdown
Collaborator

A GEO audit on 2026-09-04 scored the property at 51/100 on the root page and 43.25/100 across all eight routes, both in the tool's foundation band.

The measured categories made the diagnosis easy. Meta was 14/14 on every page. Robots allowed all 27 AI crawlers. Raw HTML carried 777 words and 19 headings without JavaScript, so the prerender is doing its job. What was missing:

Category Before
Schema 2.25 / 16
llms.txt 0 / 18
AI Discovery 0 / 6

Six of eight routes emitted no structured data at all.

Structured data on every page

The previous convention emitted Organization on the home page only, reasoning that it describes the property rather than the page, so repeating it on five routes states one fact five times. That is right about authorship and wrong about retrieval — an answer engine parses each URL independently and has no mechanism to carry the root page's entity across to /method.

The fix is a per-page @graph with stable @ids rather than copy-paste. The Organization is one entity referenced from every route, which answers the original objection properly: one fact, stated once, addressed by identity. Pages that support more say more.

Route Primary entity Also carries
/ WebPage Organization with makesOffer
/engagements/ WebPage makesOffer, FAQPage from the deck's existing questions
/experts/ CollectionPage ItemList roster linking each profile
/experts/[slug]/ ProfilePage Person, worksFor as an @id reference
/scope/ ContactPage
/method/, /matters/, /privacy/ WebPage

makesOffer rides only on the two pages that actually display the rate card.

dateModified derived from git

A date written by hand is a claim nobody verifies, and a page that says it was updated last week when it was not is the kind of small untruth this property avoids everywhere else.

scripts/content-modified.mjs records the last commit touching each route's sources before the build. A route with no recorded date emits no dateModified rather than a fabricated one. The deploy workflow now checks out full history — git log on a shallow clone reports the shallow boundary, so every page would otherwise claim the date of the most recent push.

The assumption worth reviewing: a route's inputs are its page file plus the copy deck. The deck is one file, so a copy edit anywhere moves the date on every route that reads it. Coarse, but not false — a change to a page's source inputs is a modification of that page.

AI discovery

llms.txt, /ai/summary.json and /ai/faq.json are generated from the copy deck for the same reason knowsAbout is: a hand-kept second copy of the site's structure goes stale the first time a route changes. /.well-known/ai.txt states the crawling posture explicitly and disallows /api/, which carries live matter detail written before a conflict check has run.

Deliberately not done

  • No SearchAction in the WebSite node. The property has no search endpoint, and declaring one that 404s is a claim the site cannot honour. The audit tool suggests it generically.
  • No areaServed, still. The copy nowhere states which jurisdictions this practice takes. That remains a copy decision the markup follows rather than leads.
  • geo fix --apply was not used. Its generated FAQPage is in Italian ("Cos'è forensics.rootsystem.com?"), its descriptions carry mojibake, and its SearchAction points at a route that does not exist. The plan was taken; the files were written by hand.

Needs your input

sameAs is wired on both Organization and Person, defaults to empty, and is emitted only when populated. The audit scored Social Trust 0/5 and Academic Trust 0/5 on its absence and it is the cheapest E-E-A-T signal available — but every URL has to be a profile that exists and that the practice controls, since a sameAs pointing at something that is not the same entity merges two entities in the index. LinkedIn, ORCID, Google Scholar. The fields are in src/copy/landing.ts.

Verification

  • yarn workspace @rootsystem/forensics build — clean, 11 routes prerendered including the three new endpoints.
  • astro check4 errors before, 2 after. Both remaining are pre-existing parser complaints in scope.astro.
  • All eight pages verified to emit exactly one valid JSON-LD block.

Re-audit after deploy to measure the real score against the tool's 79/100 projection. Note that llms.txt alone is 18 of those points and the tool's own output calls it "an organizational signal, not a proven ranking factor" — the schema and sameAs work is the part expected to move citations.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EDibtmcGWcytbgVpb2su7F

…hat cannot lie

A GEO audit on 2026-09-04 scored the property at 51/100 on the root page and
43.25/100 across all eight routes, both in the tool's "foundation" band. The
measured categories tell a clear story: meta was perfect at 14/14 on every page,
robots allowed all 27 AI crawlers, and the raw HTML carried 777 words and 19
headings without JavaScript. What was missing was structured data (2.25/16),
llms.txt (0/18) and AI discovery endpoints (0/6).

Structured data, on every page rather than only the root. The previous
convention emitted Organization on the home page alone, reasoning that it
describes the property rather than the page and that repeating it five times
states one fact five times. That is right about authorship and wrong about
retrieval: an answer engine parses each URL independently and cannot carry the
root page's entity across to /method. Six of eight routes emitted nothing.

The answer is a per-page @graph with stable @ids, not copy-paste. The
Organization is one entity referenced from every route, which satisfies the
original objection properly -- one fact, stated once, addressed by identity --
while giving each page its own primary entity. Pages that support more say more:
ContactPage on /scope, CollectionPage plus an ItemList roster on /experts,
ProfilePage plus Person on a profile, FAQPage on /engagements built from the
copy deck's existing questions. makesOffer rides only on the two pages that
actually display the rate card.

dateModified is derived from git rather than hand-kept. A date written by hand
is a claim nobody verifies, and a page that says it was updated last week when
it was not is the kind of small untruth this property avoids everywhere else.
scripts/content-modified.mjs records the last commit touching each route's
sources before the build; a route with no recorded date emits no dateModified at
all rather than a fabricated one. The deploy workflow now checks out full
history, because git log on a shallow clone reports the shallow boundary and
every page would claim the date of the most recent push.

llms.txt, /ai/summary.json and /ai/faq.json are generated from the copy deck for
the same reason knowsAbout is: a hand-kept second copy of the site's structure
goes stale the first time a route changes. /.well-known/ai.txt states the
crawling posture explicitly and disallows /api/, which carries live matter
detail written before a conflict check has run.

Two things deliberately not done. No SearchAction in the WebSite node -- the
property has no search endpoint and declaring one that 404s is a claim the site
cannot honour. No areaServed, still: the copy nowhere states which jurisdictions
this practice takes, and that remains a copy decision the markup follows rather
than leads.

sameAs is wired but empty, on both Organization and Person, and emitted only
when populated. It is the Knowledge Graph disambiguation signal and the audit
scored Social Trust 0/5 and Academic Trust 0/5 on its absence, but every URL has
to be a profile that exists and that the practice controls -- a sameAs pointing
at something that is not the same entity merges two entities in the index. The
fields are waiting on real URLs.

astro check goes from 4 errors to 2; the remaining two are pre-existing parser
complaints in scope.astro.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDibtmcGWcytbgVpb2su7F
Both verified before publishing. The ORCID record resolves to Rob Jacques
through the public API; the LinkedIn vanity URL is the one the profile copy was
written from. A sameAs pointing at something that is not the same entity merges
two entities in the index, so neither went in unchecked.

ORCID is the resolvable https:// form rather than the bare identifier, because
sameAs takes URLs.

No Google Scholar entry. A Scholar profile is a researcher page claimed at
scholar.google.com, not a blog or a product, and there is no reason to create
one around a single conference paper from 2000.

The Organization sameAs is still empty. It needs profiles the practice itself
controls -- a company page, a society listing -- and a personal LinkedIn is not
one of those.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDibtmcGWcytbgVpb2su7F
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant