# 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 `

`, heading order, canonical/title/description present, non-empty `alt` on every ``, 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.