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.
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.
| Error | zod / tsc | Surfaces |
|---|---|---|
| Missing field, wrong type | Yes | Build |
| Slug off-plan, duplicate order | No | 404 or shuffled index |
| English page, no localized pair | No | hreflang to an empty page |
| Link to an ungenerated route | No | Crawler 404, link equity stops |
| JSON-LD price ≠ page price | No | Answer 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.
| Assertion | What it asserts | Failure looks like | Gate |
|---|---|---|---|
| Frontmatter | code unique, order on plan, theme in five lanes | Two articles at order 126 | Block |
| Links, canonical | Every internal link resolves; canonical self-referential | Link 404, canonical on the old domain | Block |
| hreflang | Every code in each locale, both ways, x-default set | German page on a pre-migration URL | Block |
| Structured data | Required fields, valid enums, absolute URLs | offer.price and body price disagree | Block |
| Output | Sitemap, RSS, llms.txt, search all list the page | In the sitemap only | Warn |
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.
| Step | Action | Owner | Passes when |
|---|---|---|---|
| 1 List assertions | Six, from last quarter’s incidents | Engineering, content | Each has a known-bad sample that trips it |
| 2 Mount | zod, remark, build hook | Front end | A broken file fails the build |
| 3 Grade | Block, ratchet, or warn; whitelist has owner and expiry | Tech lead | Two weeks, no --no-verify |
| 4 Gate | No deploy on failure; report posts to the PR | Platform | Link 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.