GEO-126·Issue 11·Technical Foundation·126 / 132

Put GEO into CI: Validate Structured Data and Links at Build Time

Type checks pass while links 404 and JSON-LD prices drift. Five build-time assertions, three Astro mount points, and a gate that can actually fail.

5 min readGEOTechnical Foundationcross-border

Issue focusScale It: SOPs, Organization, and the 2027 Roadmap

Three weeks before Black Friday, a DTC e-scooter brand ships a new product page template. Local build passes, CI is green, deploy goes out Thursday. Monday, support forwards a chat-assistant screenshot: last season’s price, “sold out” on a model discontinued in September, a German hreflang tag on the pre-migration URL. Rollback takes four hours. The recrawl, the re-sampled answer, and the promotional email do not. Composite scene; routine failure.

Frontmatter, canonicals, hreflang pairs, internal links, and JSON-LD should fail the build, not the launch; a defect caught after deploy costs a recrawl, a re-sample, and a fresh explanation.

The type checker answers the wrong question

Astro’s content collections validate frontmatter shape with zod: field present, type right, slug on pattern. Keep it. zod tells you whether one document is well-formed, not whether it agrees with its siblings, the generated routes, or the price beside it.

GEO defects are almost all relational. A slug nobody planned. Two articles at the same order. An English page with no localized pair. A link to a route never generated. JSON-LD pricing an offer the page stopped showing. None is a type error, so none stops the build.

Errorzod / tscSurfaces
Missing field, wrong typeYesBuild
Slug off-plan, duplicate orderNo404 or shuffled index
English page, no localized pairNohreflang to an empty page
Link to an ungenerated routeNoCrawler 404, link equity stops
JSON-LD price ≠ page priceNoAnswer quotes the wrong price

Amplification makes it a release problem: one template edit touches every URL that shares it, and crawlers and assistants re-sample on schedules you do not control, so a four-hour window can keep a stale price alive for a full sampling cycle.

Five assertions, from frontmatter to rendered HTML

Assertions test a relationship or an artifact, never a file alone. Five cover most structural defects that surface on the AI side.

AssertionWhat it assertsFailure looks likeGate
Frontmattercode unique, order on plan, theme in five lanesTwo articles at order 126Block
Links, canonicalEvery internal link resolves; canonical self-referentialLink 404, canonical on the old domainBlock
hreflangEvery code in each locale, both ways, x-default setGerman page on a pre-migration URLBlock
Structured dataRequired fields, valid enums, absolute URLsoffer.price and body price disagreeBlock
OutputSitemap, RSS, llms.txt, search all list the pageIn the sitemap onlyWarn

Content gates are another list — length, heading count, whether the answer unit stands alone; the content QA checklist covers those. One asks whether we wrote it right, the other whether we wired it right.

Three mount points, four steps

Put each assertion next to the data it reads, not in a site-wide scanner.

zod owns single-entry fields; cross-entry uniqueness must load every entry. A remark plugin sees path and line number at compile time. The astro:build:done hook receives the final route table and output folder — the only layer that can prove a link resolved, JSON-LD landed in the HTML, and the sitemap included the page.

StepActionOwnerPasses when
1 List assertionsSix, from last quarter’s incidentsEngineering, contentEach has a known-bad sample that trips it
2 Mountzod, remark, build hookFront endA broken file fails the build
3 GradeBlock, ratchet, or warn; whitelist has owner and expiryTech leadTwo weeks, no --no-verify
4 GateNo deploy on failure; report posts to the PRPlatformLink and schema tickets stop

Step one gets skipped. Without a known-bad sample you test only the happy path, and a broken assertion looks like a passing one.

Example: three red checks on a green preview

Example — paths, IDs, and numbers are invented.

A comparison page is otherwise ready: frontmatter complete, answer unit written, preview clean. Three checks fail on the pull request.

✗ en/geo-999-vs-page.md:18  link /en/geo/content/compare-template/ not found
✗ jsonld  offer.price 199 ≠ page price 219  (sku AB-999)
✗ hreflang  code GEO-999 has en but no de

None is a code bug. The build passes locally, and the preview 404s only if a reviewer clicks that link. Fix order matters: publish the missing locale, move the price to one SKU record, then repair the link. Reverse it and the price ships twice.

All-blocking is no blocking

A rule has three fates: block, ratchet, or record. All-blocking and the team routes around it; all-warning and nobody reads it. Set all checks to error and week one brings legacy failures, week two someone appends || true, week three is green and untrusted.

Split by who introduced the problem: new breakage in this pull request blocks, backlog ratchets against a recorded baseline, style items go to a weekly digest. Whitelist entries record who approved and when they expire; without an expiry, a whitelist becomes a second rulebook.

Where this breaks

The fact can be wrong. JSON-LD matches the page and both come from an expired price table, so consistency approves two errors together. Master-data sync is the backstop, not another assertion.

The rendered page differs. The hook sees emitted HTML, not what a crawler receives; CDN rewrites, WAF rules, and client-side injection sit outside it. Keep one no-JS fetch and status-code check after deploy.

Citation is out of scope. Green proves parseable, not quoted. Visibility comes from sampling; the management number is the GEO scorecard.

Platforms sit outside the build: marketplace listings, feeds, third-party reviews.

Checks rot. Change the template and the assertion goes stale; unmaintained checks become false alarms and get switched off wholesale.

None of this belongs on day one. Fix visibility and answer assets with the 90-day GEO roadmap, then lock them; otherwise you padlock a system that does not exist. CI proves no regression, never growth — the rest of this line is the technical foundations hub.

This month, break one internal link in a published article by hand and run a full build. If the pipeline stays green, you do not need more checks. You need your first mount point.