# ADR-0003: Enforce SEO rules with Zod + build linter (not Pydantic)

**Status:** Accepted — 2026-07-15

## Context

We want SEO/AEO rules (title/description length, canonical, OG image, structured data, single H1,
alt text) to be *enforced*, not aspirational. The question of a Pydantic-style validator came up,
but this is a TypeScript/Astro build — Pydantic is Python and would mean bolting a foreign runtime
onto a JS pipeline.

## Decision

Enforce in two layers, both native to the stack:

1. **Zod schema** on content-collection frontmatter (Astro Content Collections already run on Zod).
   Encodes: title ≤ 55 chars, description 120–158, valid canonical, required OG image, `noindex`
   flag, `primaryTopic`. Violations **fail `npm run build`**.
2. **Build-time SEO linter** (`scripts/seo-lint.mjs`) over `dist/**/*.html` for rules Zod can't see
   in frontmatter: exactly one `<h1>`, heading order, canonical/title/description present, non-empty
   `alt` on every `<img>`, every JSON-LD block parses. Runs in CI (and locally via `npm run seo:lint`).

Do **not** introduce Python/Pydantic.

## Consequences

- Every commit is validated before ArgoCD deploys; "the rules are followed" is guaranteed by CI.
- Rules live with the content schema and the build, in one language.
- Requires the content collections from ADR-0002 to carry the shared `seo` schema.
- Detailed rules in `docs/seo-aeo-optimization.md` §1.
