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.

0/47

Foundations

What everything else sits on. Cheap to get right at the start, expensive to change once there are thirty routes.

  • Seen innext.config.ts
    hosting-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/content/site.ts/uses/ — rendered entirely from it
  • Seen insrc/lib/blog.ts
    cms-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.

  • Seen inWorkbench — Buttonsrc/styles/kit.css
    focus-visible
    .btn:focus-visible {
      outline: 2px solid var(--color-accent);
      outline-offset: 2px;
    }
  • Seen insrc/app/globals.cssBrand book — motion
    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;
      }
    }

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.tsx
    metadata-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 inAssets & meta — the templatesrc/lib/og.tsx
  • Seen insrc/app/sitemap.tssrc/app/robots.ts
    sitemap-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.ts
    sitemap-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.tsx
    jsonld
    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>
  • Seen insrc/app/blog/[slug]/page.tsxRich Results Test — paste the built URL ↗
    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 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.tsx
    fonts
    const inter = Inter({ variable: "--font-inter", subsets: ["latin"], display: "swap" });
  • Seen inpublic/projects/ — all WebP, kept that way
    image-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 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 insrc/app/not-found.tsx
  • Seen inwrangler.jsonc
    host-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
🥚 0/3