# Website Spec Compliance - Issue Creation Handoff

> **Status:** Planning / handoff document. No code changes yet.
> **Audience:** The agent that will create tracking issues from this plan (and the
> humans who triage them).
> **Source audit:** The [Website Specification checklist](https://specification.website/checklist/),
> inspected against this repo on **2026-06-17** (branch `claude/sleepy-wozniak-ie055u`).
> The checklist has **88 line items**; ~**52 are actionable gaps** for this codebase,
> the rest are already covered or not applicable (see [Do-Not-File](#do-not-file-already-covered-or-na)).

---

## 1. How to use this document (instructions for the issue-creating agent)

1. **Create one issue per item ID below in the "Actionable items" tables**, UNLESS the item
   appears in a **Suggested grouped issue** (Section 4) - in that case create the single grouped
   issue and check off the member items in its body. Do **not** create both.
2. Use the **item ID** (e.g. `WS-11`) in the issue title so issues are traceable back to this plan,
   e.g. `WS-11: Add skip-to-main-content link [a11y][Required]`.
3. Apply the **labels** from Section 6.
4. Copy the item's **Proposed work** and **Acceptance criteria** into the issue body. Add the
   **Evidence / files** line so the implementer has a starting point.
5. Assign the **Priority tier** as a label or milestone (`P1`/`P2`/`P3`).
6. **Do NOT create issues** for anything in Section 7 (Do-Not-File). If you think one of those is
   actually in scope, leave a note on this PR instead of opening an issue.
7. Where an item says **"verify"**, the issue is an investigation/audit task, not necessarily a
   code change - label it `needs-investigation`.

---

## 2. Context the implementer needs

- **Stack:** Next.js 16 App Router, React 19, Prisma 7, Postgres. Deployed behind Cloudflare.
- **Product shape:** Multi-tenant SaaS. Dealers create public landing pages served on subdomains /
  custom domains. **Organic search visibility is the core value prop** - SEO regressions are product
  regressions. SEO, structured data, and SSR are already in strong shape.
- **Exposure profile that drives prioritization:**
  - Public-facing consumer pages → **accessibility = WCAG/ADA risk**.
  - Collects PII + runs **GA4 unconditionally** → **privacy = GDPR/PECR/CCPA risk**.
  - SEO-driven → **Core Web Vitals feed ranking**.
- **Hard repo rule:** All code changes must be made in a git worktree
  (`./scripts/setup-worktree.sh <name> <branch>`), never in the main checkout.
- **ZO-number constraint:** unrelated, but note `dealerNumber` is intentionally nullable
  (see `docs/ZO_NUMBER_COMPLIANCE.md`) - don't let any new work "fix" it.

---

## 3. Prioritization framework

Priority = **checklist severity (`Required` > `Recommended` > `Optional`) × real-world exposure for
this product**, with effort used as a tie-breaker for sequencing.

- **P1 - Priority:** `Required`-severity gaps and items with concrete legal/ranking/a11y exposure.
- **P2 - Worth doing soon:** `Recommended` items with good leverage or defense-in-depth value.
- **P3 - Backlog / opportunistic:** `Optional` items, perf novelties, low-leverage polish.
- **Quick hit (⚡):** orthogonal tag - effort ≈ an afternoon or less, no product/legal decision
  required. Many P1 a11y items are also quick hits; ship those first.

**Effort key:** S = <2h · M = ~half-day to a day · L = multi-day / needs design or legal input.

---

## 4. Suggested grouped issues (epics)

To avoid 50 micro-issues, the issue agent SHOULD create these umbrella issues and fold the listed
members into them. Items NOT listed here get their own issue.

### EPIC-A - "Quick-Hit Web-Spec Batch" (one low-risk PR) ⚡ · P1/P2
Members: **WS-11, WS-12, WS-1, WS-4, WS-6, WS-49, WS-21, WS-71, WS-54, WS-55, WS-41, WS-16, WS-19, WS-35, WS-36**
These are additive, low-risk, and need no product decisions. Clears 2 Required a11y items plus a pile
of Recommended ones in a single branch. (Each member still has a full spec below for reference.)

### EPIC-B - "Privacy & Consent Compliance" · P1 · needs legal input
Members: **WS-44 (privacy policy), WS-45 (cookie consent), WS-46 (GPC), WS-47 (cookieless analytics)**
Track together because they share the consent/analytics surface and all need legal sign-off on copy
and behavior. WS-44 route is trivial; the blocker is content.

### EPIC-C - "Accessibility Verification Pass" · P1 · needs-investigation
Members: **WS-14 (contrast), WS-15 (alt-text CMS path), WS-17 (table scope), WS-18 (touch targets)**
A single runtime audit (Axe + Lighthouse + manual) that produces follow-up fix issues. Don't guess
statically - measure, then file fixes.

### EPIC-D - "Security Headers Hardening" · P2
Members: **WS-22 (COOP/COEP/CORP), WS-23 (SRI), WS-28 (Reporting-Endpoints), WS-27 (HSTS preload decision), WS-29 (CSP nonce - stretch)**
All live in `next.config.mjs` headers() / `config/csp.config.mjs`. WS-27 is a one-way decision, flag it.

### EPIC-E - "Agent & Feed Discoverability" · P2
Members: **WS-2 (RSS/Atom feed), WS-57 (machine-readable formats - satisfied by the feed), WS-58 (HTTP Link headers)**
The dealer blog already emits `BlogPosting` JSON-LD; a feed unlocks several items at once.

---

## 5. Actionable items catalog

IDs map to the master list numbering from the audit. ❌ = not covered, ⚠️ = partial/verify.

### 5.1 - P1 Priority items

| ID | Item | Sev | Effort | Quick hit | Status |
|----|------|-----|--------|-----------|--------|
| WS-11 | Skip-to-main-content link | Required | S | ⚡ | ❌ |
| WS-12 | Respect `prefers-reduced-motion` | Required | S | ⚡ | ❌ |
| WS-14 | Colour contrast audit + fixes | Required | M | | ⚠️ verify |
| WS-15 | Image alt-text on CMS/Puck output | Required | S | | ⚠️ verify |
| WS-17 | Data tables: `scope` on `<th>` | Required | S | | ⚠️ verify |
| WS-18 | Touch target sizes (24/44px) | Required | S | | ⚠️ verify |
| WS-40 | Core Web Vitals confirmed green at p75 | Required | M | | ⚠️ |
| WS-41 | Replace raw `<img>` with `<Image>` | Required | S | ⚡ | ⚠️ |
| WS-26 | Confirm infra HTTP→HTTPS redirect | Required | S | | ⚠️ verify |
| WS-27 | HSTS `preload` decision | Required | S | | ⚠️ decision |
| WS-44 | Privacy policy page | Required | M | | ❌ (issue #249) |
| WS-45 | Cookie consent + gate GA4 | Required | L | | ❌ |

**Details:**

- **WS-11 - Skip link.** No skip link exists; `<main id="main">` landmarks already present
  (`app/home/[domainSlug]/page.tsx`, `app/directory/.../[region]/page.tsx`). *Work:* add a visually-hidden,
  on-focus-visible "Skip to main content" anchor as the first focusable element in `app/layout.tsx`
  pointing at `#main`; ensure every top-level layout exposes a `#main` target. *AC:* Tab-on-load shows
  the skip link; activating it moves focus to main; works on dealer pages, directory, dashboard, apex.
- **WS-12 - Reduced motion.** No `prefers-reduced-motion` query despite keyframes in
  `tailwind.config.ts` and transforms in `app/globals.css`. *Work:* global reduce block that neutralizes
  animation/transition durations; audit notification slide + accordion + button transforms. *AC:* with
  OS "reduce motion" on, no non-essential animation plays; functionality intact.
- **WS-14 - Contrast.** Color system in `app/globals.css:18-32`; sky-blue accent `#38bdf8` on light
  surfaces and 90%-opacity secondary text are the suspects. *Work (EPIC-C):* run Axe/Lighthouse across
  dealer page, directory, registration, dashboard; raise failing pairs to ≥4.5:1 (text) / ≥3:1 (large/UI).
  *AC:* zero contrast violations on audited templates.
- **WS-15 - Alt text.** Hand-authored components are good (decorative `alt=""` used correctly). Unverified
  path is CMS/Puck-rendered images via `lib/cms/publisher.ts`. *Work:* confirm editor requires/permits alt
  and that publisher emits it. *AC:* published CMS images always carry an `alt` attribute.
- **WS-17 - Table scope.** Admin tables (`components/admin/DealerTable.tsx`, `AtRiskPanel.tsx`) use real
  `<table>`/`<th>` but `scope="col"`/`scope="row"` unconfirmed. *Work:* add scopes (+ `<caption>` where
  helpful). *AC:* headers programmatically associated.
- **WS-18 - Touch targets.** Needs runtime check; inputs use 12px padding. *Work (EPIC-C):* verify
  interactive controls meet ≥24×24 (target 44×44) CSS px. *AC:* documented pass or fix list.
- **WS-40 - Core Web Vitals.** GA4 gives partial field data; no dedicated `web-vitals` reporting. *Work:*
  confirm LCP ≤2.5s / INP ≤200ms / CLS ≤0.1 at p75 on a representative dealer page (CrUX/Lighthouse);
  file fixes if failing. Consider `useReportWebVitals` (see WS-40b note in P2). *AC:* documented p75 status.
- **WS-41 - Raw img.** `components/DealerTemplate.tsx:346` uses a raw `<img>` for the dealer logo while the
  rest use `<Image>`. *Work:* convert to `<Image>` with explicit dimensions (note `images.unoptimized:true`
  is intentional - Cloudflare handles optimization, so keep that). *AC:* no raw `<img>` on the dealer template.
- **WS-26 - HTTP→HTTPS.** App sets HSTS + hard-codes `https://` in dealer canonical redirects; the plain
  HTTP→HTTPS hop is assumed to be Cloudflare. *Work:* confirm infra enforces it. *AC:* documented confirmation.
- **WS-27 - HSTS preload.** Header is `max-age=31536000; includeSubDomains` (no `preload`). *Work:* decide
  whether to add `preload` + submit (irreversible across all subdomains incl. dealer customs). *AC:* decision
  recorded; header updated only if approved.
- **WS-44 - Privacy policy.** No `/privacy` route; `sitemap.ts:204` has a TODO referencing **#249**. *Work
  (EPIC-B):* add route + link from footers + sitemap entry. *Blocker:* legal copy. *AC:* reachable, linked,
  in sitemap.
- **WS-45 - Cookie consent.** GA4 (`components/analytics/GoogleAnalytics.tsx`) loads unconditionally. *Work
  (EPIC-B):* consent banner; do not load GA / set non-essential storage before opt-in (EU/UK); persist choice;
  provide withdrawal. *AC:* no non-essential cookies before consent in EU/UK; choice persists.

### 5.2 - P2 Worth doing soon

| ID | Item | Sev | Effort | Quick hit | Notes |
|----|------|-----|--------|-----------|-------|
| WS-1 | `<meta name="theme-color">` | Recommended | S | ⚡ | Add to Next `viewport` export (light/dark via media) |
| WS-4 | `<meta name="color-scheme">` | Recommended | S | ⚡ | Add `colorScheme` to viewport export |
| WS-5 | Canonical on `comingsoon.html` | Recommended | S | ⚡ | App routes already set canonical |
| WS-6 | SVG favicon + apple-touch-icon + maskable | Recommended | S | ⚡ | Have PNG+ICO only; add to `app/icon`/metadata |
| WS-7 | `og:image` on root layout + directory pages | Recommended | S | | Dealer pages already have it; reuse `app/og` route |
| WS-16 | `role="alert"`/`aria-describedby` on form errors | Required* | S | ⚡ | Enhancement to existing error UI; *core is covered |
| WS-19 | `inputmode`/`enterkeyhint` on mobile inputs | Recommended | S | ⚡ | type + 16px font already present |
| WS-21 | `/.well-known/security.txt` | Recommended | S | ⚡ | First file in a new `public/.well-known/` |
| WS-49 | Web app manifest (`app/manifest.ts`) | Recommended | S | ⚡ | Pairs with WS-6 |
| WS-54 | `/llms.txt` | Recommended | S | ⚡ | Curated index of key pages |
| WS-55 | Named AI-crawler rules in `app/robots.ts` | Recommended | S | ⚡ | GPTBot, ClaudeBot, CCBot, Google-Extended |
| WS-71 | `/.well-known/change-password` redirect | Optional | S | ⚡ | Accounts exist → applicable; 1-line redirect |
| WS-35 | `scrollbar-gutter: stable` | Recommended | S | ⚡ | Prevents layout shift |
| WS-36 | Dynamic viewport units (dvh/svh/lvh) | Recommended | S | ⚡ | Replace full-height `vh` usages |
| WS-2 | RSS/Atom feed for dealer blogs | Recommended | M | | Unlocks WS-57; blog JSON-LD already present |
| WS-57 | Machine-readable content format | Recommended | - | | Satisfied by WS-2 feed |
| WS-58 | HTTP `Link:` headers for discovery | Recommended | M | | Advertise sitemap/feed/llms in proxy/next.config |
| WS-22 | COOP / COEP / CORP headers | Recommended | M | | `next.config.mjs` headers() |
| WS-23 | Subresource Integrity on external scripts | Recommended | M | | Stripe, GA, OAuth; CSP already allow-lists |
| WS-28 | Modern Reporting API (`Reporting-Endpoints`/`report-to`) | Recommended | S | | Currently legacy `report-uri` only |
| WS-29 | CSP nonce-based (drop `unsafe-inline`/`unsafe-eval`) | Recommended | L | | Stretch/hardening; higher effort in Next |
| WS-30 | Conditional requests (ETag/Last-Modified/304) | Recommended | M | | Custom route `app/api/static/[...path]` |
| WS-46 | Honor Global Privacy Control | Recommended | M | | EPIC-B; read `Sec-GPC` / `navigator.globalPrivacyControl` |
| WS-47 | Privacy-respecting / cookieless analytics | Recommended | M | | EPIC-B; consider GA consent-mode or cookieless tool |
| WS-48 | Data minimisation review | Recommended | M | | Review dealer fields collected/retained |
| WS-51 | Maintenance page + 503 + `Retry-After` | Recommended | M | | Health endpoint returns 503 but no maintenance UX |
| WS-52 | Graceful degradation when JS fails | Recommended | M | | Core is SSR; add `<noscript>` where appropriate |
| WS-53 | External monitoring + status page | Recommended | M | | Health endpoint only; host status page off-infra |

\* WS-16: the parent checklist item "Accessible form errors" is `Required` and largely covered; this issue
is the remaining enhancement (programmatic announcement). Label `enhancement` + `a11y`.

### 5.3 - P3 Backlog / opportunistic

| ID | Item | Sev | Notes |
|----|------|-----|-------|
| WS-10 | Sitemap index files | Recommended | Single sitemap fine under 50k URLs; revisit at scale |
| WS-13 | `hidden="until-found"` for collapsibles | Recommended | Nice-to-have for find-in-page |
| WS-20 | `:user-invalid`/`:user-valid` selectors | Recommended | `aria-invalid` already used |
| WS-42 | BFCache: scope `beforeunload` to edit pages | Recommended | Already intentional on edit pages only |
| WS-31 | `No-Vary-Search` header | Recommended | Low leverage here |
| WS-32 | Speculation Rules | Recommended | Perf novelty |
| WS-33 | View Transitions | Recommended | Perf novelty |
| WS-34 | `content-visibility` off-screen | Recommended | Perf novelty |
| WS-37 | CSS containment | Optional | Perf novelty |
| WS-38 | Scroll-driven animations | Optional | Perf novelty |
| WS-39 | Compression Dictionary Transport | Optional | Advanced |
| WS-8 | Image/video sitemap extensions | Optional | |
| WS-9 | IndexNow | Optional | ISR revalidation covers most needs |
| WS-50 | Offline support / service workers | Optional | |
| WS-56 | Per-page Markdown source endpoints | Recommended | Low value (not a docs site) |
| WS-59 | Agent Skills discovery | Recommended | Emerging/draft |
| WS-60 | `/llms-full.txt` | Optional | After WS-54 if useful |
| WS-61 | Content-Signals in robots.txt | Optional | Emerging |
| WS-62 | Web Bot Auth | Optional | |
| WS-63 | MCP / tool discovery endpoint | Optional | `mcp/` dir is dev tooling only |
| WS-64 | A2A agent cards | Optional | |
| WS-65 | DNS-AID | Optional | |
| WS-66 | NLWeb | Optional | |
| WS-67 | WebMCP | Optional | |
| WS-68 | Schemamap | Optional | |
| WS-70 | `/.well-known/api-catalog` | Recommended | Only if a public API surface is intended |
| WS-72 | `/.well-known/traffic-advice` | Optional | Prefetch-proxy opt-out |
| WS-24 | DNS CAA records | Recommended | **DNS-level** - registrar task, file as `ops`/`infra` |
| WS-25 | DNSSEC | Optional | **DNS-level** - registrar task, file as `ops`/`infra` |
| WS-80 | International URL structure review | Recommended | Single-language today; revisit only if i18n is planned |
| WS-79 | hreflang in XML sitemaps | Optional | hreflang already in HTML head (acceptable) |

> **Note on WS-40b (optional sub-task):** if WS-40 reveals a need for ongoing field telemetry, file a
> follow-up to add `useReportWebVitals` reporting. Keep it out of the initial issue unless needed.

---

## 6. Suggested labels

- Category: `a11y`, `seo`, `security`, `performance`, `privacy`, `resilience`, `agent-readiness`,
  `well-known`, `i18n`, `infra`
- Severity (mirror the spec): `spec:required`, `spec:recommended`, `spec:optional`
- Priority: `P1`, `P2`, `P3`
- Workflow: `quick-hit`, `needs-investigation`, `needs-legal`, `needs-decision`, `enhancement`
- Tracking: `website-spec` (apply to ALL issues from this plan for easy filtering)

---

## 7. Do-Not-File (already covered or N/A)

**Already covered - do not open issues:** HTML doctype, `<html lang>`, `<meta charset>`, viewport, `<title>`,
meta description (app routes), canonical (app routes), Open Graph (dealer pages), robots.txt, XML sitemaps,
URL structure, redirects (301/308), SSR/SSG, soft-404 avoidance, meta-robots/X-Robots-Tag, heading hierarchy,
internal linking, JSON-LD structured data, breadcrumbs, HSTS, upgrade-insecure-requests, CSP (present),
`report-uri`, nosniff, X-Frame-Options + `frame-ancestors`, Referrer-Policy, Permissions-Policy, cookie
attributes (`__Host-`/`__Secure-`, HttpOnly, SameSite), lazy loading, preload/preconnect, Cache-Control,
compression, `next/font` swap, critical CSS, script `afterInteractive`, custom 404/500 pages, form labels,
keyboard nav, visible focus indicators, semantic landmarks, descriptive link text, icon-button `aria-label`s,
accessible auth (OAuth + password managers + Turnstile), no a11y overlays (good), hreflang (en-US/en-CA),
third-party script lockdown via CSP.

**Not applicable to this site - do not open issues unless scope changes:**
- Captions/transcripts (no audio/video).
- Most i18n: localised metadata, `lang` on inline content, language switcher, RTL/bidi, writing-modes/CJK,
  locale-aware content, plural rules, IDN - single-language (en-US/en-CA) site.
- Well-known: `webauthn` (no passkeys), `openid-configuration` (not an OIDC provider), `apple-app-site-association`
  (no iOS app), `assetlinks.json` (no Android app), `webfinger`/`nodeinfo` (not federated).
- Popover API (WS-3): existing custom implementation is accessible; migration is optional polish, not a gap.

---

## 8. Recommended sequencing for later sessions

1. **EPIC-A (Quick-Hit Batch)** - one PR, near-zero risk, clears 2 Required a11y items + ~12 Recommended.
2. **EPIC-C (A11y Verification Pass)** in parallel - produces concrete fix issues for WS-14/15/17/18.
3. **EPIC-B (Privacy & Consent)** - start the legal-copy thread immediately; implement behind it.
4. **EPIC-D (Security Headers)** and **EPIC-E (Feed/Discovery)** - independent, schedule as capacity allows.
5. **P3 backlog** - opportunistic; pull in alongside related feature work.

---

*Generated from the Website Specification checklist audit. Update this doc if the audit is re-run or the
site's scope (e.g. adds a second language, native apps, or passkeys) changes.*
