---
title: "audit skill: measure, fix, re-measure, stop"
description: "The verification loop for websites built with void. Explains when to run `void lint` (static, seconds, after each significant edit) and `void audit` / `void…"
canonical: https://void-design.vercel.app/docs/audit
lastModified: 2026-09-16
---

# audit — measure, fix, re-measure, stop

`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

1. **Run `void lint` after every significant edit.** It's static, needs no browser, and finishes in seconds. Fix its errors before moving on.
2. **Run `void audit` only against a production build** (`next build` + `next start`, or a deployed preview). **Never against `next dev`**: dev ships unminified JS, HMR and no prefetch, so the numbers are meaningless.
3. **Use `--start "cmd" --port N`** so void starts the server, waits for it, audits, and stops it. Don't leave background servers running, and don't `pkill -f "next start"` (it can kill your own shell).
4. **Read `--format md`** (compact, grouped by rule, top 5 targets each). Use `--json` only when parsing programmatically.
5. **Fix in order: gates → errors → budgets → warnings.** `info` findings are review prompts and never fail a run.
6. **Group by rule id, not by occurrence.** One root cause (a layout component, a token, a helper) usually explains every instance. Fix it once.
7. **Look up unfamiliar ids with `void rules <id>`** (why, fix, example) before editing. Don't guess.
8. **Re-run only the category you touched** (`void a11y …`, `void seo …`), then one full `void audit` at the end.
9. **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.
10. **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 <id>` · `void rules perf` (list a category) | instant | nothing |
| Finished a page or feature | `void <category> --start "<start cmd>" --port <N>` for the categories you touched | 20–90 s | production build |
| Before saying "done" | `void audit --start "<start cmd>" --port <N> --format md` | 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 <url> --out ./dna` (see the `inspire` skill) | 30–60 s | URL |

Next.js recipe:

```bash
bun run build                                                        # or: npx next build
void audit --start "bun run start -p 3100" --port 3100 --format md    # or: npx next start -p 3100
```

If `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 `<head>`, 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:** `0` pass · `1` findings ≥ `--fail-on` or budget exceeded · `2` usage/config error · `3` URL unreachable or server didn't start · `4` browser failed (run `void doctor`).
- **"Could not complete"** lists audits that crashed. Treat that category as unmeasured, not as passing.

## Fixing efficiently

1. Take the top group. Run `void rules <id>` if the fix isn't obvious.
2. Find the source: `target.file:line` for lint; `selector` or `html` for runtime findings. Search the codebase for the selector's class names or the text.
3. Fix the **shared** cause: the layout, the `pageMetadata` helper, the Button component, the token, the font loader.
4. `void lint`, rebuild, then re-run **that category only**: `void seo --start "…" --port N --format md`.
5. Repeat until the category passes, then move to the next. Finish with one full `void audit`.

Tips:
- `--routes /,/pricing,/blog/hello` audits representative templates (one per route type), not every page.
- `--profile mobile` halves perf/smooth time while iterating. The final run uses both profiles.
- `--runs 5` when perf numbers are near a budget. Lab variance of ±10–20% between runs is normal. Trust medians, not a single run.
- `--max-per-rule 2` keeps the markdown output short in big reports.
- Rebuild after every code change before re-auditing. `next start` serves 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`

```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`.
