feat: upgrade to Astro 7; harden the content schema; wire a11y linting

Amends D1 to pin the major explicitly (v7.x) rather than inherit it. The
^5.0.0 pin was recalled rather than checked and was two majors stale the day
it was written, which meant shipping a framework carrying high-severity XSS
advisories. CLAUDE.md now requires every version pin to be verified against
the registry, and R11 requires re-checking at each build-order boundary.

npm audit now reports 0 vulnerabilities, down from 16. Every Astro advisory
is cleared; the residual 10 all traced to @lhci/cli, which is removed — it
was the sole source of 7 high-severity findings, 0.15.1 is latest so there
was no clean upgrade, and it cannot run without pages or a lighthouserc.
Re-added at build step 7 with a freshly verified pin.

Content collections migrated to the Content Layer API: src/content.config.ts,
loader: glob(), z from astro/zod.

Two review passes found seven defects in the fix itself, all now closed:

- z.coerce.date() read an unquoted 20260801 as epoch milliseconds and
  yielded 1970-01-01 silently; the first replacement then accepted
  2026-13-45 as an Invalid Date and rolled 2026-02-30 over to 2026-03-02.
  Dates are now anchored, date-only, parsed as UTC and round-tripped.
- The title bound applied the SEO spec's 50-60 to the headline rather than
  the rendered <title>, which guaranteed 68-78 on every article and rejected
  all five planned launch headlines. Articles are now the documented
  exception: the headline is the <title>, no suffix.
- An article could ship an image with no alt text, or whitespace-only alt.
- Two schema comments asserted controls nothing enforced; both are now real
  refinements, each tested with a failing and a passing case.
- PRACTICE_SLUGS and PRACTICE_AREAS could drift silently; a compile-time
  check now catches both directions.
- eslint.config.js imported globals and @eslint/js undeclared, resolving by
  hoisting accident.
- scripts/deploy-local.sh claimed parity with CI while skipping npm run
  check and two credential guards — on the only path this site can ship
  today.

Accessibility linting is on (36 jsx-a11y rules) before step 1 writes the
layout. An earlier claim in §7 that none was possible was wrong twice, and
is corrected in AGENTS.md entry (t) along with the reasoning.

Opens Q30 and Q31 for two unregistered claims in src/data/site.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XquaEq4BgWMCwUqLEyNkF
This commit is contained in:
Pouya Lajevardi
2026-08-26 14:10:09 -04:00
co-authored by Claude Opus 5
parent 0d8b63380a
commit 7514a49803
15 changed files with 3684 additions and 5959 deletions
+15 -2
View File
@@ -296,8 +296,21 @@ you do not settle · how to prepare.
Astro content collection, MDX. Index reverse-chronological with topic filtering
by practice area.
Article frontmatter: `title`, `description`, `publishDate`, `updatedDate`,
`topics[]`, `practiceAreas[]`, `readingTime`, `draft`.
Article frontmatter: `title`, `seoTitle` (optional), `description`,
`publishDate`, `updatedDate`, `topics[]`, `practiceAreas[]`, `readingTime`,
`image` and `imageAlt` (both optional, but `imageAlt` is **required whenever
`image` is set**), `draft`, `reviewedByPouya`.
`title` is the headline and, for articles, the `<title>` — they carry no
` · Pouya Lajevardi` suffix; see `04-seo-spec.md` for why. `seoTitle` replaces
it when a headline that reads well falls outside 5060. `src/content.config.ts`
enforces the rendered length and names the offending string in the error.
Dates are date-only ISO (`2026-08-01`), parsed as UTC and round-tripped, so a
typo fails the build rather than shipping as 1970 or as the wrong day.
`reviewedByPouya` carries D9: the schema refuses to build an entry with
`draft: false` and `reviewedByPouya: false`.
Content territories, from brief §VII: process explainers · regulatory commentary ·
industry-specific dispute commentary · anonymised reflections · technical
+9
View File
@@ -43,6 +43,15 @@ Every page passes through one `SEO` component. A page without it is not finished
```
title 5060 chars, unique. Pattern: "<Page> · Pouya Lajevardi"
Home: "Pouya Lajevardi · Mediation & Arbitration · Toronto"
ARTICLES ARE THE EXCEPTION: no " · Pouya Lajevardi" suffix.
The suffix is 18 chars, so a headline that already reads
5060 renders at 6878 — over this ceiling. Measured against
the five launch headlines in 03-content-spec.md, the suffix
rule fails 5 of 5; without it, 4 of 5 pass. An article's
headline IS its <title>; `seoTitle` in the frontmatter
overrides it when a headline that reads well is out of range.
src/content.config.ts enforces this and names the offending
string and its length in the build error.
description 140160 chars, unique, written for a human, not stuffed
canonical absolute, https, trailing slash
og:title/description/image/url/type/site_name/locale (en_CA)