website-qa

The pre-launch QA gate for a new site: run the basic tests (a11y light+dark + seo + navigation + anchors + orphans + images + tone + positioning + placeholders + email + links), fix to green, then han

SKILL.md

GitHub

Website QA

The gate every page passes before launch, and the check re-run after any edit. Two parts: the automated basic tests, then the performance handoff.

1. Basic tests (fix to green)

npm test runs the Playwright suite from the new-website skill's templates/astro/ overlay against a production build (astro build && astro preview — the static output Cloudflare serves). Test the contract, not the cosmetics; red → green → commit.

  • a11y.spec.ts — axe WCAG 2 A/AA + best-practice on every page, light AND dark; zero violations.
  • seo.spec.ts — title ≤60 / description 120–160 (hard floor AND cap); og/twitter mirror title/description; canonical == og:url == absolute URL; one <h1>; JSON-LD parses; the sitemap matches PAGES exactly (drift alarm: a route missing from PAGES fails here — the website-seo-geo contract).
  • navigation.spec.ts — every PAGES route returns 200; internal links resolve with no redirect hop (absolute URLs on the production host count as internal).
  • anchors.spec.ts — the other half of internal-link integrity: every in-site #fragment (same-page #x, cross-page /page#x, relative about#x, or /?q#x) points at a real element id on its target page — links and shipped-page target ids are read from the live DOM. Catches the dead-anchor bug navigation.spec can't see — a link that 200s but scrolls nowhere because the id doesn't exist. Offline.
  • orphans.spec.ts — the inverse of navigation.spec: every PAGES route must be reachable from the home page by following internal links. A page can be in the sitemap and pass every other spec yet be linked from nowhere (orphaned — crawlers find it late, humans never do). Crawls the in-site link graph from / and fails listing any unreachable page. Deliberately-unlinked pages opt out via ORPHAN_EXEMPT. Offline. The judgment-side report (inbound count, thin pages, where to add links) is the internal-link-audit skill.
  • middleware.spec.ts — functions/_middleware.ts, which astro preview never runs, called directly: previews noindexed, live-domain responses unchanged, and the project alias <project>.pages.dev 301ing to the live domain only once CANONICAL_URL is set. Offline.
  • analytics-server.spec.ts — analytics server mode, the middleware and functions/go/email.ts called directly with a stand-in for Plausible: one page view per page a visitor opens and none for assets, prefetches, scripts or previews; an email tap counted and answered with the mailto: link; crawlers and links from other sites given the "[at]" hint, never the address. Offline.
  • images.spec.ts — every <img> has alt + width/height + an explicit loading (lazy, or eager for the LCP image); raster sources are WebP/AVIF in src, srcset and <picture> (the website-design-system rules).
  • tone.spec.ts — no em dashes / contractions / buzzwords in copy or metadata. Language-aware: the contraction/buzzword rules apply only to <html lang> en* pages; the em-dash ban is universal (the website-content-guide rules).
  • positioning.spec.ts — each page in the POSITIONING map carries its positioning term (case-insensitive) in <title>, <meta description> and its <h1> or intro paragraph; declared body phrases (market category + core positioning phrase) appear in the body. Hermetic string containment, not a density check (the website-positioning spine). Empty map = green from commit 1. Also warns (without failing) when a PAGES route has no entry and isn't in POSITIONING_EXEMPT — an un-positioned content page is a lost opportunity, surfaced rather than silent. This is a mechanical string check, not a judgment read — for "does the offer actually read clearly", see website-positioning-check.
  • placeholders.spec.ts — leftovers a visitor could read. Reads each page's rendered copy, <head> metadata, alt texts, aria-labels and JSON-LD, plus the web manifest, for an unfilled [SLOT] (the "[MISSING: …]" content token and the starter's [BRACKET] slots alike), filler text, an author note (TODO), an unrendered template expression or a dummy name. The CI grep for [MISSING: is the fast source-side half; this is the half that reads what a page serves. The starter's privacy page, imprint and manifest sit in UNFILLED_UNTIL_LAUNCH and are reported, not failed, until filled; a filled target fails until its entry is deleted, so the list empties itself and must be empty at launch (new-website §4). A "[MISSING: …]" on a page only warns on your own machine, so a draft branch can be pushed, and fails in CI. Not read: the 404 page, llms.txt, and the starter's example values (the name Example, hello@example.com); those stay a launch-checklist item.
  • privacy-host.spec.ts — the host the privacy page tells visitors to block (the text marked data-analytics-host) is the host in ANALYTICS.scriptHost. A [BRACKET] slot there passes (placeholders.spec.ts reports it), and so does a page without the mark.
  • email.spec.ts — no plaintext (harvestable) email address in the served HTML (addresses go through <EmailLink>, which obfuscates; the website-design-system rule).
  • tables.spec.ts — every table is in a .table-wrap of one kind (stack or figures) with its region, label and tabindex; stacked tables carry their roles and a label equal to each column header; no page or table scrolls sideways at 360/320px; a stacked table is a table again from 641px; no cell sets hyphens: auto. Tests the CSS on a fixture too, so a site with no table still proves it. See website-design-system, "Tables".
  • links.spec.ts — offline outgoing-link guard: no STALE_DOMAINS (domains you've retired) reappear in an anchor href. Hermetic — makes no network calls. The liveness of the links you keep is the network-dependent outgoing-link-audit skill (pre-launch / monthly, not in CI).

First fill tests/_helpers.ts PAGES with the real routes. A failure is a real defect — fix the page, do not loosen the test (Rule 12: never silently skip). For diagnosing a specific failure, test-failure-triage; for intermittent ones, flaky-test-debugger.

1b. TDD — what NEW tests to write for a feature

The specs in §1 are the always-on baseline. When you add a feature, write its test first (red → green → commit): state what "correct" means before building it, then implement until green. Test the contract/behaviour, not the cosmetics (assert "the form shows a success state", not "the heading is 32px") — a test that can't fail when the logic breaks is worthless (Rule 9).

When a new route is added, the only change needed for the baseline to cover it is adding its path to tests/_helpers.ts PAGES — a11y, SEO, navigation and image checks then apply automatically.

Feature → the high-value test to add:

Feature Write a test that…
New page/route add it to PAGES; baseline a11y+SEO+links now assert it (returns 200, one <h1>, title/desc in range). Then link it from a related page — orphans.spec.ts fails if it's reachable from no internal link.
Contact / lead form valid input → success state; invalid → error; the endpoint/mailto is invoked. The mail-app contact form: install website-contact-form, whose tests/mail-form.spec.ts covers exactly this (set its PAGE and TO). The skill is bundled with a site only when it was scaffolded with a form; otherwise it comes from the toolkit the site was built with.
Auto-rotating photos (hero carousel) the rotation timing on a frozen clock, no page jump when the photo changes, the dots readable on a pale and a dark photo, a way to stop it, keyboard focus. Install website-hero-carousel, whose tests/hero-carousel.spec.ts covers exactly this (set its PAGE, PAUSE and SWAP). In dots pause mode a dot choice stops the rotation until reload (keyboard focus entering also stops it, and reduced motion starts it stopped), and the WCAG 2.2.2 trade-off is described in that skill's SKILL.md, section "Pause", which is where an audit looks.
Theme toggle / dark mode toggling persists across reload; run a11y in both themes (THEMES=['light','dark']). A toggle that stores the choice is storage the privacy page must disclose (section 3).
Site search a known query returns the expected result; empty query handled.
Redirects (_redirects) each legacy URL 301s to the new clean URL.
Images / new media every <img> has alt + width/height; raster sources are WebP/AVIF; LCP image not lazy (see website-design-system).
Nav / CTA change the expected links render and resolve; the CTA lands on the right page.
Copy/brand rules extend the tone rules in tests/_helpers.ts to ban the new off-brand phrasing (tone.spec.ts holds the pages to them).
New/changed positioning add the page's term to the POSITIONING map in positioning.spec.ts; the page must carry it in <title>/<meta description>/<h1> (see website-positioning).
Public scorecard (website-scorecard) install the skill by its §2 (three files and the package script; the spec needs the script, so never copy it alone), then set PAGE in tests/scorecard.spec.ts: the page must show the numbers in scorecard.json, and the file must add up.
Colours or a logo from website-brand-kit nothing to copy: its intake runs this suite itself after it writes, and puts every file back if the suite goes red. After a run with a logo, the seven icon files the pages and the manifest link exist under public/ (favicon.svg, favicon.ico, apple-touch-icon.png, icon-192.png, icon-512.png, icon-maskable-512.png, images/logo.png).
Story-led home page (website-story) copy that skill's templates/story.spec.ts into tests/ and fill its CONFIG from the values listed at the end of STORY.md (the rest have defaults); positioning.spec.ts stays untouched.
Tagline under the logo (website-tagline) copy that skill's templates/tagline.spec.ts into tests/ and set CONFIG.selector to the element that shows the line: the page must show SITE.tagline word for word, in a <p>, no other file under src/ may hold the line, and the line must keep to that skill's limits (eight words, 50 characters).
Exposed email/phone it's rendered via EmailLink — assert the built HTML has no plaintext mailto: or literal @-address (crawler/spam protection; see website-design-system).

Keep it thin: a marketing site needs a high-value harness, not 1000 unit tests. Don't test a cosmetic you'll change next week; do pin every contract a third party could silently break.

1c. Enforce the gate on push (the pre-push hook)

The baseline only protects the site if it actually runs before a deploy. The scaffold ships a pre-push git hook (scripts/hooks/pre-push) that runs scripts/verify.mjs (the same as npm run verify: npm ci only on its first run or when package.json or package-lock.json changed, npm run check, then npm test, which builds once; short output) and check_seo.py (if present), and refuses the push if anything is red — so a broken build never reaches the deploy branch. A site without scripts/verify.mjs gets the older npm run build + npm test. It's wired automatically: the prepare script in package.json (node scripts/wire-hooks.mjs, a line with no shell syntax) points core.hooksPath at scripts/hooks on npm install — when the site is the root of its git repo. In a subfolder of a bigger repo it leaves that repo's hooks alone, so the gate is not wired there; say so rather than assume it runs.

This is the local complement to CI: Cloudflare Pages builds on push independently of CI, so without this hook a red test would still deploy. The hook is what makes "fails → does not ship" literally true on a direct-push-to-main workflow, without forcing a slower PR flow.

Offer it as a choice — never silently impose or remove it (Rule 1/Rule 12). When setting up or handing off a site, surface both directions:

  • Relax for one push: git push --no-verify (skips the hook for that push only).
  • Disable entirely: git config --unset core.hooksPath (re-enable with npm install, or git config core.hooksPath scripts/hooks).
  • Tune what it runs: edit scripts/hooks/pre-push (e.g. leave out a slow step; CI still runs everything).

A team that wants the speed of direct pushes and the safety of the gate keeps it on; a solo builder mid-experiment may want it off. Make the call explicit with the user; don't decide for them. (Requires the per-machine npx playwright install chromium from SETUP.md, or the test step errors — a fail-loud, fixable by setup, not a reason to drop the hook.)

2. Performance — Lighthouse / PageSpeed (FCP & LCP)

Performance numbers need a real run; this is a handoff to the user (the agent cannot run Lighthouse against a deployed URL reliably). Ask the user to run one:

Please run a PageSpeed Insights test ( https://pagespeed.web.dev/ ) on the page URL — or Chrome DevTools → Lighthouse — and paste the First Contentful Paint (FCP) and Largest Contentful Paint (LCP) for mobile and desktop.

Targets (Core Web Vitals "good"): FCP ≤ 1.8s, LCP ≤ 2.5s (mobile, field data preferred). Then act on what the report flags. Common fixes already baked into the starters / website-design-system:

  • LCP image: eager-load + fetchpriority="high", correct format/size, no lazy.
  • Render-blocking CSS: Astro inlines it (inlineStylesheets: 'always').
  • Fonts: self-hosted woff2, font-display: swap, preload only above-the-fold.
  • Layout shift (CLS): explicit width/height on media.
  • Defer non-critical JS (analytics is async plus a short inline stub, production-only; none in analytics server mode).

For deeper technical SEO (crawl, indexability, Core Web Vitals strategy), run the existing seo-audit skill — do not duplicate it here.

Done means

All basic tests green; FCP/LCP within target on the user's PageSpeed run (or the remaining gaps explicitly listed, not hidden); launch checklist in new-website §4 satisfied.