47 checks · run it on any site
The boring bits, and whether this site has actually had them done.
Not a build sequence — a list to run against a site. A new one before it ships, or one you've just inherited and need to know the state of. Name the site, work down it, come back in a fortnight and the ticks are still there. The parts it keeps referring to live in the Workbench.
Foundations
What everything else sits on. Cheap to get right at the start, expensive to change once there are thirty routes.
- Seen innext.config.tshosting-shape
// next.config.ts const nextConfig: NextConfig = { output: "export", images: { unoptimized: true }, // required — no optimiser in a static export trailingSlash: true, turbopack: { root: import.meta.dirname }, // pin: stray lockfiles higher up confuse it }; - Seen insrc/lib/blog.tscms-build-cache
// src/lib/blog.ts — cacheBust is never read by the GROQ query, only by the cache const BUILD_NONCE = String(Date.now()); function fetchFresh<T>(query: string, params = {}): Promise<T> { return client.fetch<T>(query, { ...params, cacheBust: BUILD_NONCE }); } - Seen insrc/app/layout.tsx
Components & interaction
The states people forget. Check them on the built thing, not the design — that's where they go missing.
- focus-visible
.btn:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } - Seen inWorkbench — Button, loading
- reduced-motion
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } } - Seen inWorkbench — Empty state
- Seen inWorkbench — Field
SEO & metadata
Twenty minutes of work that decides whether any of it gets found. Usually the part an inherited site is missing entirely.
- Seen insrc/app/layout.tsxmetadata-base
export const metadata: Metadata = { metadataBase: new URL(site.url), title: { default: `${site.name} — ${site.role}`, template: `%s — ${site.name}` }, description, alternates: { canonical: site.url, types: { "application/rss+xml": "/feed.xml" } }, robots: { index: true, follow: true }, }; - Seen insrc/lib/seo.ts
- Seen insrc/app/sitemap.tssrc/app/robots.tssitemap-robots
// src/app/sitemap.ts import type { MetadataRoute } from "next"; import { site, projects } from "@/content/site"; export default function sitemap(): MetadataRoute.Sitemap { return [ { url: `${site.url}/`, lastModified: lastEdited("src/app/page.tsx") }, ...projects.filter(p => p.deepDive).map(p => ({ url: `${site.url}${p.deepDive}/` })), ]; } - Seen insrc/app/sitemap.tssitemap-lastmod
function lastEdited(...paths: string[]): Date | undefined { try { const iso = execFileSync("git", ["log", "-1", "--format=%cI", "--", ...paths], { encoding: "utf8" }).trim(); return iso ? new Date(iso) : undefined; } catch { return undefined; // shallow clone or no git — omit rather than invent } } - Seen insrc/app/robots.tssrc/app/card/page.tsx
- Seen insrc/app/layout.tsxjsonld
const jsonLd = { "@context": "https://schema.org", "@graph": [ { "@type": "WebSite", "@id": `${site.url}/#website`, url: site.url, name: site.name, publisher: { "@id": `${site.url}/#person` } }, { "@type": "Person", "@id": `${site.url}/#person`, name: site.name, url: site.url, image: `${site.url}/icon.svg`, sameAs: [site.links.linkedin] }, ], }; // <Script id="ld" type="application/ld+json">{JSON.stringify(jsonLd)}</Script> - jsonld-entities
// layout defines it once { "@type": "Person", "@id": `${site.url}/#person`, name, url, image, sameAs: [...] } // every article points at it — no second copy author: { "@id": `${site.url}/#person` }, publisher: { "@id": `${site.url}/#person` }, - Seen insrc/components/Breadcrumbs.tsx
- Seen insrc/app/layout.tsx
- Seen inWorkbench — Prose
- Seen innext.config.ts
Performance
Cheap wins first, and measure before touching anything else. Every one of these is checkable in a few minutes on a production build.
- Seen insrc/app/layout.tsxfonts
const inter = Inter({ variable: "--font-inter", subsets: ["latin"], display: "swap" }); - Seen inpublic/projects/ — all WebP, kept that wayimage-optimise
sharp("shot.png").resize({ width: 1100 }).webp({ quality: 80 }).toFile("shot.webp"); - Seen insrc/components/checklist/Checklist.tsx
- lighthouse
npm run build && npx serve out npx lighthouse http://localhost:3000 --preset=desktop --view - bundle
npx -y @next/bundle-analyzer@latest # or: ANALYZE=true npm run build
Accessibility
The non-negotiable floor. All of it is quick, and all of it is the sort of thing nobody notices until someone can't use the site.
- Seen inWorkbench — Skip link
- Seen insrc/app/globals.css
The last mile
The final pass before handover — and the first pass on a site you've taken over, because it's the group that most often turns out to have been skipped.
- Seen inAssets & meta
- Seen insrc/app/not-found.tsx
- Seen inwrangler.jsonchost-404
// wrangler.jsonc "assets": { "directory": "./out", "not_found_handling": "404-page" } - Seen insrc/app/brand/page.tsx
- Seen insrc/app/layout.tsx
- Seen inREADME.md
- Seen insrc/app/sitemap.ts