When Claude Code loads it
The verification loop for websites built with void. Explains when to run `void lint` (static, seconds, after each significant edit) and `void audit` / `void perf|smooth|a11y|seo|geo|design` (real Chromium against a production build, before declaring done), how to read findings, severities, gates, scores and budgets, how to fix efficiently with `void rules <id>`, how to configure void.config.ts, when to stop, and how to run it in CI. Use before telling the user a website, landing page or UI change is finished; when asked to audit, test, check, verify, benchmark or score a site for performance, Core Web Vitals, accessibility, SEO or AI search; when a `void` command prints findings or exits non-zero; when setting performance budgets; or when adding a GitHub Actions quality gate.
void is the CLI in this plugin (packages/cli, binary void, runs on Bun ≥ 1.2 with a local Chromium). Every finding has a stable rule id (perf/lcp-slow, seo/metadata-in-body, lint/tw-v3-arbitrary-var). The fixes live in the speed, seo, a11y, craft and motion skills. This skill is about the loop.
Full flag reference: references/commands.md. CI: references/ci.md.
The 10 rules that matter most
- Run
void lintafter every significant edit. It's static, needs no browser, and finishes in seconds. Fix its errors before moving on. - Run
void auditonly against a production build (next build+next start, or a deployed preview). Never againstnext dev: dev ships unminified JS, HMR and no prefetch, so the numbers are meaningless. - Use
--start "cmd" --port Nso void starts the server, waits for it, audits, and stops it. Don't leave background servers running, and don'tpkill -f "next start"(it can kill your own shell). - Read
--format md(compact, grouped by rule, top 5 targets each). Use--jsononly when parsing programmatically. - Fix in order: gates → errors → budgets → warnings.
infofindings are review prompts and never fail a run. - Group by rule id, not by occurrence. One root cause (a layout component, a token, a helper) usually explains every instance. Fix it once.
- Look up unfamiliar ids with
void rules(why, fix, example) before editing. Don't guess. - Re-run only the category you touched (
void a11y …,void seo …), then one fullvoid auditat the end. - Fix the user experience, not the metric. Never hide content from the lab, special-case headless browsers or user agents, delay loading until after measurement, or delete a feature to make a finding disappear.
- Stop when the stopping criteria below are met. Don't chase the last point of a noisy perf score with risky refactors. Report remaining warnings to the user instead.
When to run what
| Moment | Command | Time | Needs |
|---|---|---|---|
| After editing components, CSS, metadata, config | void lint (or void lint src) |
1–5 s | nothing |
| Unsure what a finding means | void rules · void rules perf (list a category) |
instant | nothing |
| Finished a page or feature | void for the categories you touched |
20–90 s | production build |
| Before saying "done" | void audit --start " |
1–4 min | production build |
| Deployed preview / production | void audit https://preview-url.example --format md |
1–4 min | URL |
| Setup problems (no Chromium, launch failure, exit code 4) | void doctor |
5 s | nothing |
| Compare before/after | --out before.json, fix, --out after.json, then void report after.json --format md |
— | saved reports |
| Borrow a reference site's design system | void extract (see the inspire skill) |
30–60 s | URL |
Next.js recipe:
bun run build # or: npx next build
void audit --start "bun run start -p 3100" --port 3100 --format md # or: npx next start -p 3100If void.config.ts defines start, routes and src (the template's does), just run void audit --format md.
Categories
| Category | What it measures | Typical first fixes |
|---|---|---|
perf |
LCP, CLS, TBT, FCP, TTFB, JS/CSS/font/image bytes, requests, third parties, DOM size, bfcache, cache headers, compression (median of --runs, mobile + desktop) |
speed skill: LCP image, client JS, fonts |
smooth |
Scripted scroll (dropped frames, long animation frames), scripted clicks (INP), layout shifts after input or scroll, animated layout properties, reduced-motion, idle rAF loops, WebGL contexts | speed references rendering-smoothness.md, react-pitfalls.md |
a11y |
axe-core WCAG 2.2 AA + best practices, keyboard focus visibility, target size on mobile, 320px reflow, zoom disabled | a11y skill |
seo |
Status codes, redirects, soft 404, title/description/canonical in , h1, links, OG and icons, robots.txt, sitemap, hreflang, HTML size |
seo skill + next-metadata.md |
geo |
Raw-HTML vs rendered content (bot UAs), streamed/hidden content, JSON-LD validity/required props/visibility, AI bot robots policy, llms.txt, Markdown mirrors, answer-first/citation heuristics | seo skill + structured-data.md, ai-crawlers.md |
design |
Font families, type scale, line length, contrast of body text, palette and accent sprawl, spacing grid, radius sprawl, dark mode, AI-slop tells, horizontal overflow | craft skill |
lint |
Static source: Tailwind v4 correctness, Next 16 APIs, React render-body bugs, a11y markup, SEO files, motion hygiene | the id's skill |
Reading a report
**FAIL** (fail-on error) · overall 78 · perf 71 · smooth 88 · a11y 92 · seo 49 · geo 80 · design 85
3 errors, 9 warnings, 4 info · mobile+desktop
Budgets exceeded: jsKb@mobile 212 KB > 170 KB
**ERROR** `seo/canonical-missing` ×4: Canonical link is missing
- No <link rel="canonical"> in <head> of raw HTML · `http://localhost:3100/pricing`
- Fix: Add alternates.canonical per page via pageMetadata().
- Docs: https://void-design.vercel.app/rules/seo/canonical-missing- Severity:
error= broken for users or crawlers (fails with the default--fail-on error);warn= measurable loss;info= heuristic or low-evidence advice. - Gates: a few failures cap the category score at 49 no matter what else passes. SEO gates:
seo/http-status,seo/noindex,seo/canonical-missing,seo/canonical-invalid,seo/canonical-broken,seo/robots-blocks-all,seo/sitemap-url-status. GEO gates:geo/js-dependency(main content not in raw HTML),geo/jsonld-parse-error. Perf: poor LCP (> 4 s mobile, > 2.5 s desktop) or CLS > 0.25. Fix these first. - Budgets: any metric over its budget fails the run (exit 1) even with zero error findings.
- Scores (0–100): per category, then weighted overall (perf 25%, a11y 20%, smooth 15%, seo 15%, design 15%, geo 10%). Perf and smooth blend metric curves with findings; other categories subtract per-finding penalties (each rule counted at most twice).
- Exit codes:
0pass ·1findings ≥--fail-onor budget exceeded ·2usage/config error ·3URL unreachable or server didn't start ·4browser failed (runvoid doctor). - "Could not complete" lists audits that crashed. Treat that category as unmeasured, not as passing.
Fixing efficiently
- Take the top group. Run
void rulesif the fix isn't obvious. - Find the source:
target.file:linefor lint;selectororhtmlfor runtime findings. Search the codebase for the selector's class names or the text. - Fix the shared cause: the layout, the
pageMetadatahelper, the Button component, the token, the font loader. void lint, rebuild, then re-run that category only:void seo --start "…" --port N --format md.- Repeat until the category passes, then move to the next. Finish with one full
void audit.
Tips:
--routes /,/pricing,/blog/helloaudits representative templates (one per route type), not every page.--profile mobilehalves perf/smooth time while iterating. The final run uses both profiles.--runs 5when perf numbers are near a budget. Lab variance of ±10–20% between runs is normal. Trust medians, not a single run.--max-per-rule 2keeps the markdown output short in big reports.- Rebuild after every code change before re-auditing.
next startserves the old build. - A finding that you've verified is a false positive, or an intentional choice: turn it off in
void.config.ts→rules: { "design/no-dark-mode": "off" }with a comment explaining why. Never do this for gates.
void.config.ts
// void.config.ts (project root; auto-detected). Plain object: no import needed.
/** @type {import('@void/cli/config').VoidConfig} */
const config = {
kind: "marketing", // default budgets: "marketing" (JS ≤170 KB) or "app" (JS ≤300 KB)
src: "src", // also used by `void lint` with no args, and linted during `void audit`
start: { command: "bun run start -p 3100", port: 3100, readyPath: "/", timeoutMs: 30000 },
routes: ["/", "/pricing", "/blog/hello-world", "/this-page-does-not-exist"], // include a 404 probe
profiles: ["mobile", "desktop"],
runs: 3,
budgets: { // override defaults per profile; ms, KB (transfer), CLS unitless
mobile: { lcp: 2500, cls: 0.1, inp: 200, tbt: 200, ttfb: 800, jsKb: 170, cssKb: 25, fontKb: 120, imageKb: 1000, htmlKb: 30 },
desktop: { lcp: 1500, cls: 0.05, inp: 100, tbt: 100, ttfb: 600 },
},
rules: { "geo/no-statistics": "off" }, // severity override: "error" | "warn" | "info" | "off"
failOn: "error", // "error" | "warn" | "info" | "never"
lighthouse: false, // true = also run Lighthouse (optional peer dep: bun add -d lighthouse)
};
export default config;CLI flags override the file (--kind app, --fail-on warn, --routes, --runs, --profile).
Stopping criteria (declare done only when all hold)
| Check | Target |
|---|---|
void lint |
0 errors |
void audit exit code |
0 (no errors, all budgets pass) against a production build, both profiles |
| Gates | none failed in any category |
| perf score | ≥ 90 mobile on marketing routes (≥ 80 app routes) |
| a11y score | 100 (0 axe violations), plus a manual keyboard pass over dialogs, menus and forms |
| seo score | ≥ 95 |
| geo score | ≥ 90. info heuristics reviewed, not necessarily fixed |
| smooth | no smooth/* errors; scroll jank and INP warnings fixed or explained |
| design | no errors; warnings reviewed against the chosen direction (craft skill) |
When a target can't be met for a real reason (a required third-party script, a client-mandated video hero), say so explicitly in your summary with the measured number and the cause. Don't silence the rule.
Anti-patterns: fixing the metric instead of the experience
| Don't | Why it's wrong | Do instead |
|---|---|---|
| Detect headless/Lighthouse/bot UAs and serve a lighter page | Cloaking. Real users stay slow; search engines penalize it | Make the real page light |
Delay all JS with setTimeout(…, 5000) so TBT drops |
INP and real interactions get worse | Ship less JS, lazyOnload third parties, facades |
Start the hero at opacity: 0 to push LCP onto something else |
Users see a blank hero | Visible hero, animate secondary elements |
Add aria-label everywhere to silence axe |
Wrong or duplicated names hurt screen-reader users | Visible text, correct native elements |
aria-hidden or display:none on offending content |
Removes content for users | Fix contrast, names or structure |
Add FAQPage or fake aggregateRating to "complete" JSON-LD |
Policy violation; no rich result | Only mark up visible, true facts |
Set failOn: "never", raise budgets or turn off rules to go green |
The gate stops protecting users | Fix, or document an explicit exception |
Audit next dev, or a page with a warm cache only |
Wrong numbers | Production build, default cold runs |
| Delete a feature to clear a warning (e.g. remove all motion) | Loses the design intent | Fix the implementation (compositor-only, gated, reduced-motion aware) |
Output to the user
Finish with a short summary: the command run, pass/fail, scores per category, budgets that were close (within 10%), what you fixed (grouped by rule id), and what remains (with ids and reasons). Include the report path if you used --out.