Skip to main content
ASoc
Tutorial

A Website Requirements Checklist Where Every Line Can Fail

Seven requirements written as pass/fail acceptance criteria, each with the number this storefront actually measured — and the defects that changed the list.

The ASoc Team8 min read

A website requirements checklist is only useful if every line can fail. "Fast", "accessible" and "secure" are wishes; "Lighthouse accessibility 100 on every route", "one <h1> per page" and "no external script host that is not named in the CSP" are requirements, because a build can check them. Below is the list we hold this storefront to, with the measured result next to each line.

Most requirements checklists are written before the site exists and read once. That is the wrong order of operations. The useful ones are written as acceptance criteria — a number, a command, a pass/fail — and they are edited every time the site teaches you something. Ours has been edited by defects: a blog index with no <h1>, a 255 KiB image in a 33 KiB slot, a database client shipped to pages with no login. This post is the version of the list that survived.

If you are past requirements and about to ship, use the launch checklist instead — it is the executable pre-deploy pass. This one is the spec that comes first.

Requirements as acceptance criteria

Each row below has a category, the requirement as a testable statement, and the evidence from this codebase. The last column is the point: a requirement with no evidence is a hope.

CategoryRequirement, stated so it can failMeasured here
AccessibilityLighthouse accessibility 100 on every indexable route, desktop and mobile100 on all 8 sampled page types, both presets
StructureExactly one <h1> per route, no skipped heading levelsSweep of 16 sampled routes: all pass
PerformanceLab performance 100 desktop, 89+ mobile; CLS 0Desktop 100 on all 8; mobile 89–99; CLS 0
WeightNo image served at more than ~2× its rendered sizeCover art 255 KiB → 33 KiB
SearchPer-page title, description, canonical, sitemap entry, robots rulesrc/app/sitemap.ts, src/app/robots.ts
SecurityA Content-Security-Policy in which every allowed host is named and justified11 directives in next.config.ts
Content truthEvery link target and file reference resolvesEnforced by npm test

Two things to notice. Every threshold is a number or an existence check, and none of them is "looks good". And the measured column is honest about the misses: mobile performance is 89–99, not 100, and the reason is documented rather than hidden — see the Lighthouse accessibility write-up for how we treat the score versus the underlying defects.

Accessibility: the requirement that found real bugs

The requirement is "100 on every indexable route". Getting it on the pages that existed at launch was easy. The value came from re-running it on pages added later, which turned up three defects:

  1. /blog had no <h1> at all. The index title used SectionHeading's default h2, so the page's subject was unstated for crawlers and screen-reader users. The fix was one prop: as="h1", with card titles dropped from h3 to h2 to keep the outline contiguous.
  2. /docs jumped from h1 to h3. The first section heading in DocsContent is now h2.
  3. BlogTag missed AA contrast by a hair. text-primary (#465fff) on bg-primary-25 (#f2f7ff) at 12px measures 4.49:1; AA needs 4.5:1. Moving the text to text-primary-600 fixed it.

Nothing in that list is exotic, and none of it was caught at design time. Each was caught because the requirement was "every route" rather than "the pages we looked at". Write the scope into the requirement.

Performance: state the budget per asset, not per page

A page-level score tells you something is wrong; it does not tell you what. The requirements that moved our numbers were per-asset:

AssetBeforeAfterRequirement it implies
Product cover in a card255 KiB, 2120×132533 KiB WebP, 1060wServe a variant sized for the slot
Site logo (every page)70 KiB, 681×2399 KiB, 272×95Source pixels ≤ ~2× rendered size
Hero art87 KiB45 KiB, plus an 800w srcset alternateOffer a smaller candidate for small screens
Marketing images (19)1793 KiB550 KiBRight format for the content

The rule underneath is that the original stays for the job that needs it. The cover JPEG is still full size because it is the og:image and the product schema image; components render generated -card.webp and -view.webp derivatives instead. The build step is one command:

npx tsx scripts/images/build-variants.ts

npm test fails if a product is missing its variants, so the requirement cannot rot when someone adds product number 112.

The single largest win was not a compression setting. The first card on /templates was loading="lazy", and that card is the page's LCP element. The browser could not discover the image until layout ran, which cost 1.67 seconds of load delay. TemplateCard now takes a priority prop that the first card of a page-opening grid sets; every other card stays lazy. Requirement: the LCP element is never lazy-loaded. It is a one-line check that a component review can hold.

The requirement you only find by reading the bundle

Both the site header and the ownership lookup imported the Supabase client at module scope. That put roughly 68 KiB gzipped (255 KiB parsed) of auth code in the initial bundle of /, /blog, /docs and /pricing — pages with no account UI. The fix is src/lib/supabase/lazyClient.ts, which imports the client dynamically and skips it entirely when no sb-*-auth-token cookie exists.

The requirement it produced: a page with no feature X does not ship the code for feature X. It sounds obvious. It is rarely written down, and it is the sort of thing a requirements list is for. Server-side gates such as RLS and authorizeDownload are unchanged, because a loading shortcut must never be the thing that protects data.

Security: name every host

A generic "site must be secure" requirement fails the moment someone asks how you would know. The testable version is a Content-Security-Policy where each allowed origin is justified. In next.config.ts the policy is built from an explicit list, and the comment block above it records why each host is present: the LemonSqueezy overlay script, its checkout iframe, the Vercel analytics host, the preview iframes, and the Supabase origin derived from an environment variable rather than hard-coded.

The policy also states its own compromise. 'unsafe-inline' is allowed on script-src and style-src because Next.js injects an inline runtime script and this app has an inline theme bootstrap. A nonce-based policy would force every route to render per request and break static generation. Write that trade-off into the requirement instead of pretending the policy is stricter than it is. Two more lines are cheap: frame-ancestors 'none' with X-Frame-Options: DENY as the old-browser fallback, and Strict-Transport-Security set. preload is left off on purpose — it is a hard-to-reverse commitment that only takes effect after manual submission.

Search: crawlability is a requirement, not a plugin

The search requirements are short and all of them are files:

  • src/app/robots.ts allows / and disallows the account routes (/dashboard, /login, /signup, /forgot-password, /reset-password, /auth), mirroring the noindex metadata on those pages.
  • src/app/sitemap.ts lists every indexable route with a lastModified that is a real date, never the build clock. Telling a crawler that 100+ URLs changed on every deploy is a checkable false claim, and npm test asserts every sitemap date is midnight UTC — something a clock reading never is.
  • A page that is noindex is also absent from the sitemap. A URL you ask Google not to index and also submit is a contradiction.

Per-page titles and descriptions belong here too; the meta tags guide covers the field limits.

How to write your own list

  1. Start from user-visible failures, not framework features: cannot read it, cannot find it, cannot pay, cannot trust it.
  2. State a threshold and a scope. "Accessibility 100 on every indexable route" beats "accessible".
  3. Attach a command. Lighthouse for scores, a grep for headings, a test for file existence. If you cannot name the command, the requirement is a wish.
  4. Record the measured value beside it, including the misses.
  5. Add a line every time a defect ships. The list should grow from evidence.

Troubleshooting

SymptomLikely causeFix
Lighthouse 100 locally, 96 Best PracticesAnalytics beacon 404s off-platformRe-measure on the deployed domain; the audit is errors-in-console
Accessibility passes but a page has no <h1>Score audits do not require exactly one headingAdd a heading-outline sweep across all routes
LCP image loads lateIt is loading="lazy"Set priority on the first card of the grid
Big JS bundle on static pagesAuth or SDK client imported at module scopeImport dynamically inside an effect, gated on a cookie
Sitemap dates change on every deploynew Date() in lastModifiedUse declared or content-derived dates
Small text fails contrast by ~0.01Brand colour on a tint at 12pxUse the next colour step down

Frequently asked questions

How many requirements should a website checklist have? Few enough that each one has an owner and a command. Seven to ten categories with a measurable line each beats a fifty-item document nobody reruns.

Should the checklist include design requirements? Only the testable ones: contrast ratio, minimum tap target, a maximum rendered image size. "Looks modern" belongs in a brief, not a checklist.

Is a Lighthouse score a requirement or a symptom? Both. The score is a fast alarm; the requirement is the underlying property, such as one <h1> per page or an LCP image that is not lazy. Hold the property, and use the score to notice regressions.

Does buying a template satisfy these? It gives you a measured baseline to start from, not a pass. Re-run every line against your own content and your own images, because that is where the defects arrive.

Templates that start with a baseline

The templates below ship with real routes, per-page metadata, sitemap and robots files, and accessible markup, so the requirements above start from a passing state instead of from zero.

Keep reading

Tutorial8 min read

What Is Supabase Used For? Four Things, in One Real App

Four things, precisely: auth, row-level authorization, private storage, atomic writes -- including the rate-limit race this app actually hit and fixed.

Read more
Tutorial8 min read

What Is a Web Page Template? Two Products Share the Word

A hosted design you edit in a builder, and a source repo you own, are both called templates. What is actually inside one, from a catalog of 111.

Read more