bcs-website/docs/adr/0003-seo-enforcement-with-zod.md
Brad Rodgers cd91c4b97c Initial commit: BlueCap Strategies Astro site
Static Astro 5 site for BlueCap Strategies with exact live-site content
parity, deployed to k3s via GitOps (ArgoCD).

- Page copy in Markdown content collections (services, focus-areas, pages,
  insights) with a Zod SEO schema enforced at build time
- Homepage + About restored to exact live copy; real live-site imagery
- Build-time SEO linter (scripts/seo-lint.mjs) and Playwright e2e suite
- Multi-stage Dockerfile (nginx serves dist/) and Kustomize manifests (k8s/)
- Per-agent robots.txt; config-driven PostHog + Umami analytics scaffold
- ADRs and engineering docs under docs/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 17:40:46 -04:00

31 lines
1.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 120158, 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.