Build checklist

Follow this while you build. Ordered the way the work happens, so it can be read top to bottom once and again at the end of each stage.

Every line here has a matching line in the production gate, under the same id. If a project is sent back, the id in the report points straight at the instruction below that would have prevented it.

Dimension
Severity
Build phase

Showing all 66 rules

Setup

Text and interface contrast meets AA

blocker a11y

4.5:1 for body text, 3:1 for text at 24px or 19px bold and above, 3:1 for interface components and focus indicators. Check text sitting on images, video and gradients by hand, because scanners cannot see those.

Done when: No contrast violation is reported, and text over imagery is checked manually at its worst point rather than its best.

A visible focus indicator on every focusable element

blocker a11y

Define one global focus-visible style in the base layer, not per component. Never set outline none without an equally visible replacement. Per-component focus styling leaves every unstyled control with whatever the browser happens to do.

Done when: Tabbing through a page shows an obvious indicator on every stop, with at least a 3:1 contrast against its background, including over images and gradients.

Ships in wp-starter/focus-tokens - import it rather than rebuilding it.

Why this rule exists, and how to satisfy it

No credential ever reaches the repository or the web root

blocker practices

Keep every key, password, host and licence in an environment file that is git-ignored and excluded from deployment. Whatever the deploy filter does not exclude ends up served over HTTPS - a licence file has been fetched from a live site this way.

Done when: No credential appears in git history or in the deployed web root. Requesting the environment file over HTTP returns 403 or 404.

Why this rule exists, and how to satisfy it

Markup and structure

Every form control has a real label

blocker a11y

Use a label element associated with its control, not a placeholder and not an aria-label standing in for one. A placeholder disappears the moment someone types, which is exactly when they need it. Add autocomplete tokens on identity and address fields.

Done when: Every input, select and textarea has an associated label element that is visible. Placeholders, where present, are supplementary rather than the only name.

Why this rule exists, and how to satisfy it

Pagination controls are links, not buttons

blocker colibrity

Only when the listing has more items than fit on one page.

Render each page control as an anchor with a real href to that page's URL. A button type="button" is invisible to crawlers and to anyone who opens links in a new tab. Keep the existing accessible markup around it - nav with an accessible name, an ordered list, aria-current on the active page.

Done when: Every page control in the pagination nav is an a element with an href that resolves to a 200. Middle-clicking page two opens it in a new tab.

Ships in wp-starter/archive-state - import it rather than rebuilding it.

Why this rule exists, and how to satisfy it

Doctype and a correct lang attribute on every page

blocker html

Start every document with the HTML5 doctype and set lang on the html element to the page's actual language. On a multilingual site the value changes per page rather than being hard-coded in the header partial.

Done when: Every sampled page has the doctype and a lang attribute whose value matches the language of the visible copy.

All dynamic output is escaped at the point of output

blocker practices

Escape every value as it is printed, using the function that matches its context - HTML text, attribute, URL, or a filtered allow-list for rich text. Escape late, at output, not early on input.

Done when: No template prints an unescaped dynamic value. A field containing a script tag renders as visible text rather than executing.

A block is three files sharing one slug

required colibrity

Every block ships as template, stylesheet and script under the same slug, and the ACF layout name, the data-block attribute, the SCSS class and the script filename all derive from it. Use the scaffold command rather than creating the files by hand.

Done when: For each block, the four names agree. No block has a stylesheet or script with no template, or a data-block value that matches no SCSS class.

JavaScript targets data attributes, never CSS classes

required colibrity

Select elements for behaviour and animation through data- attributes. CSS classes are for styling only. This is what lets a component be restyled without breaking its animation.

Done when: No querySelector or querySelectorAll in block or module scripts selects by a styling class. Renaming a BEM class in the stylesheet breaks nothing at runtime.

Page regions use landmark elements

required html

Use header, nav, main, aside and footer for what they are. One main per page and it wraps the page content. Give multiple navs distinct accessible names. Drop redundant role attributes that repeat the element's own semantics.

Done when: Each sampled page exposes exactly one main landmark, all content sits inside a landmark, and no two navigation landmarks share a name.

Lists are lists and tables are tables

required html

Mark repeated items as ul or ol. Use table only for tabular data, with th and scope, never for layout. Card grids and pagination are lists.

Done when: No layout is built from table elements, every table has header cells with scope, and repeated content is inside a list element.

Ids are unique on the rendered page

required html

Watch for ids baked into a partial that repeats. A repeated block with a hard-coded id silently breaks label association, aria references and anchor links.

Done when: The validator reports no duplicate id on any sampled page, including pages that render the same block more than once.

No inline event handlers or javascript: URLs

required html

Bind behaviour in scripts, not in onclick attributes. Inline handlers block any meaningful Content Security Policy and scatter behaviour across templates.

Done when: No rendered page contains an on* event attribute or an href starting with javascript:.

One h1 per page, and no skipped heading levels

required html

Give each page exactly one h1 naming the page. Descend one level at a time. If a heading looks wrong at its correct level, that is a styling problem, not a structure problem.

Done when: Each sampled page has exactly one h1, and its heading outline never jumps a level going down, for example h2 straight to h4.

Class names follow BEM and match their block slug

recommended html

Name classes block__element--modifier, where the block name is the block's own slug. A class that names its appearance rather than its role gets stale the first time the design changes.

Done when: Every block's root class equals its slug, and no class name outside the design system describes a colour or a size.

SEO head and schema

Every indexable page declares a self-referencing canonical

blocker seo

Emit exactly one canonical link per page pointing at that page's own absolute URL, on the production domain, with the scheme and host the site actually serves.

Done when: Each sampled page returns one canonical, absolute, on the live host, equal to the URL that was requested once redirects have resolved.

Automated check: seo-status:canonical

Filter combinations do not become a crawl trap

required colibrity

Only when the page has a listing, archive or search with filters.

Decide per filter whether its URLs should be indexed. Filters that match real search demand stay indexable and appear in the sitemap. Sort orders, free-text search and multi-value combinations canonicalise to the unfiltered listing or carry noindex. Write the decision down in the project handover.

Done when: A filtered URL either canonicalises to the base listing or is deliberately indexable and present in the sitemap. No filter combination generates an unbounded set of indexable URLs.

Why this rule exists, and how to satisfy it

Paged views are canonical to themselves

required colibrity

Only when the listing has more items than fit on one page.

Point the canonical of /page/2/ at /page/2/, never back at page one, and put the page number in the title. Canonicalising every page to the first one tells the crawler the other pages do not exist.

Done when: The canonical on page two equals page two's own URL, and page two's title differs from page one's.

Automated check: seo-status:canonical

Multilingual sites declare reciprocal hreflang

required seo

Only when the site serves more than one language.

Emit hreflang alternates on every translated page, including a self-reference and an x-default. Every alternate must point back, or search engines discard the whole set.

Done when: Each language version lists every other version plus itself plus x-default, and following any alternate lands on a page that lists the original back.

Automated check: seo-status:hreflang

Open Graph and Twitter card tags are complete

required seo

Emit og:title, og:description, og:type, og:site_name, og:locale, og:url and an absolute og:image, plus twitter:card. A relative og:image renders as no image at all when shared.

Done when: All eight tags are present on a sampled page, og:url matches the canonical, and the og:image URL is absolute and returns an image of at least 1200 by 630.

robots.txt exists and points at the sitemap

required seo

Ship a robots.txt that disallows sandbox, styleguide and any internal preview route, and names the sitemap with an absolute URL.

Done when: /robots.txt returns 200, contains a Sitemap: line with an absolute URL that itself returns 200, and does not disallow any route that should rank.

Automated check: seo-status:robots

The sitemap lists every indexable URL and nothing else

required seo

Generate the sitemap rather than maintaining it by hand. Exclude anything that is noindex, redirected or gone. Hand-maintained sitemaps go stale the first week nobody remembers them.

Done when: The sitemap index returns 200, every child returns 200, a sample of ten listed URLs returns 200 with no redirect, and no listed URL carries noindex.

Automated check: seo-status:sitemap, seo-status:tax-sitemaps

Structured data is present, valid and singular

required seo

Emit one Organization or LocalBusiness node for the site and a BreadcrumbList on pages that have a breadcrumb trail. One node per type per page - a plugin plus a theme both emitting Organization is a validation error, not redundancy.

Done when: Google's Rich Results Test reports no errors on the home page and one interior page, and exactly one Organization node exists per page with a name matching the brand.

Automated check: seo-status:org-one, seo-status:org-brand, seo-status:org-sameas

Interaction and state

Everything works with a keyboard alone

blocker a11y

Operate the whole site with Tab, Shift-Tab, Enter, Space and Escape. Menus, modals, carousels, filters, video controls and custom selects all included. Nothing may need a mouse or a hover.

Done when: Every interactive control can be reached and operated by keyboard, focus never enters a hidden element, and no component traps focus except a modal, which releases it on Escape.

Why this rule exists, and how to satisfy it

prefers-reduced-motion is honoured everywhere

blocker a11y

Check the media query in every animated module and in CSS, and honour it. Reduced motion means the content arrives in its final state, not that the animation runs faster. Parallax, auto-playing carousels and scroll-driven reveals are the ones that cause harm.

Done when: With reduced motion enabled at OS level, no element animates on scroll or entrance, all content is visible and in its final position, and nothing auto-plays.

Ships in wp-starter/motion-guard - import it rather than rebuilding it.

Why this rule exists, and how to satisfy it

Filters write their state to the URL

blocker colibrity

Only when the page has a listing, archive or search with filters.

Keep one named list of filter parameters. On any filter change, write the active values into the query string with history.replaceState and delete the parameter when a filter is cleared. On load, read the query string back and restore both the filter UI and the result set before the first paint of results.

Done when: Changing a filter changes location.search. Reloading that URL restores the same result set and the same visible filter UI. Clearing a filter removes its parameter rather than leaving it empty. Copying the URL to another browser reproduces the view.

Ships in wp-starter/filter-url-state - import it rather than rebuilding it.

Why this rule exists, and how to satisfy it

Every page of a listing has its page number in the URL

blocker colibrity

Only when the listing has more items than fit on one page.

Render real server-side pagination at /page/2/ and up. Load-more, if present, is an enhancement layered over it and must advance the URL as it loads each batch. A listing whose only navigation is an offset load-more button does not satisfy this rule.

Done when: Page two is reachable at a distinct URL containing the page number, that URL loads the correct items with JavaScript disabled, and its page number is visible in the address bar after using load-more.

Ships in wp-starter/archive-state - import it rather than rebuilding it.

Why this rule exists, and how to satisfy it

Modals trap focus, return it, and close on Escape

required a11y

Only when the project has a modal, drawer or off-canvas menu.

Move focus into the dialog on open, keep it inside while open, close on Escape, and return focus to the control that opened it. Mark the dialog and hide the rest of the page from assistive technology.

Done when: Opening a modal moves focus inside, Tab cycles only within it, Escape closes it, and focus returns to the trigger.

Ships in wp-starter/dialog-a11y - import it rather than rebuilding it.

Errors say what is wrong and how to fix it

required a11y

Describe the problem in words next to the field, announce it to assistive technology, and move focus to the first error. Colour alone is not an error message.

Done when: Submitting an invalid form produces a text message identifying the field and the fix, the message is announced, and focus lands on the first invalid control.

Why this rule exists, and how to satisfy it

Filter changes do not litter the back button

required colibrity

Only when the page has a listing, archive or search with filters.

Use history.replaceState for filter changes, not pushState. Ten filter clicks must not cost the visitor ten presses of the back button to leave the page.

Done when: After changing filters five times, one press of the browser back button leaves the listing page entirely rather than stepping back through filter states.

Ships in wp-starter/filter-url-state - import it rather than rebuilding it.

Why this rule exists, and how to satisfy it

Animation is cheap and stops when off screen

required perf

Animate transform and opacity only. Pause any infinitely repeating tween while its section is out of view. Use svh rather than lvh on pinned sections. Do not add per-block resize handlers.

Done when: No looping animation runs while off screen, a performance trace shows no layout thrash during scroll, and no animation drives a layout-triggering property.

Scripts clean up after themselves on page transition

required practices

Only when the project uses client-side page transitions.

Every module returns or registers a teardown that removes its listeners, kills its animations and disconnects its observers. Without it, navigating back and forth stacks duplicate handlers until the page stutters.

Done when: Navigating between two pages five times and returning leaves the same number of listeners and scroll triggers as the first load.

Media and payload

Images carry alt text that earns its place

blocker a11y

Describe what the image conveys in context. Decorative images take an empty alt so they are skipped. Do not write "image of". An image inside a link describes the destination.

Done when: Every image has an alt attribute, decorative ones are empty, and reading the alt text of a page aloud conveys the same information as seeing it.

Why this rule exists, and how to satisfy it

Video and audio carry captions and controls

required a11y

Only when the project has video or audio with speech.

Caption anything with speech. Give any media longer than five seconds a pause control. Auto-playing media must be muted and stoppable. Captions must be accurate, not machine output nobody read.

Done when: Every video with speech has captions that match the audio, and every auto-playing item can be paused with a keyboard.

Fonts are subset woff2, self-hosted, with a swap policy

required perf

Ship woff2 only, subset to the characters the site actually uses, self-hosted, with font-display swap and a preload for the one font used above the fold. An unsubset variable font can be several hundred kilobytes on its own.

Done when: No font file over 100 KB, no ttf or otf served to browsers, font-display is set, and at most one font is preloaded.

Why this rule exists, and how to satisfy it

Images ship in a modern format at the size they display

required perf

Serve WebP or AVIF with a fallback, generate responsive sizes, and use srcset and sizes so a phone never downloads a desktop image. A full-width PNG hero is the single most common cause of a slow mobile LCP on these projects.

Done when: No image transfers more than twice the bytes needed at its displayed size, and the image audits report no meaningful savings from format or sizing.

Below-the-fold images lazy load, the LCP image does not

required perf

Put loading lazy on images below the fold and never on the LCP image, which instead takes fetchpriority high. Lazy-loading the hero delays the very metric it is meant to help.

Done when: Every below-the-fold image has loading lazy, the LCP image has neither lazy nor async decoding and carries fetchpriority high.

Why this rule exists, and how to satisfy it

Video is deferred and never blocks the first paint

required perf

Only when the project has background or hero video.

Give the video a poster, set preload none until it is near the viewport, keep autoplay muted and playsinline, and start playback after load so the poster carries LCP. Above-the-fold autoplay video is usually the reason mobile LCP cannot be fixed by anything else.

Done when: The video's own bytes are not on the critical path, LCP resolves to the poster or another element, and total transfer on first load stays inside the page budget.

Content and language

Every dynamic component handles empty, long and broken

required practices

Design for zero items, one item, many items, a very long title, a missing image and a failed request. Test with the real content, not the placeholder set that always has three items of equal length.

Done when: Emptying a listing shows a deliberate empty state rather than a blank region. A title of double the expected length does not break its layout.

Editable content is editable, not baked into templates

required practices

Anything the client will plausibly want to change - copy, images, links, section order - comes from fields. Hard-coded strings turn every text tweak into a developer task and a deploy.

Done when: A content editor can change the text and images of every public page, and reorder the sections of a flexible page, without a developer.

Titles and descriptions are present, unique and the right length

required seo

Give every template a title pattern and every published page a description. Titles run roughly 30 to 60 characters, descriptions 70 to 160. Do not ship the theme defaults.

Done when: No sampled page is missing a title or description, no two sampled pages share either, and lengths sit in range.

The site publishes an accessibility statement

recommended a11y

Publish a page stating the conformance target, known gaps and a contact route for accessibility problems. Under the EAA this is what a client is asked for first when challenged, and it is cheap to produce at handover and expensive to reconstruct later.

Done when: A linked, reachable page names the standard targeted, lists known exceptions, and gives a working contact address.

The heading outline reads as an outline

recommended seo

Write headings that describe the section beneath them. Headings chosen for their size are a styling decision wearing a structural tag.

Done when: Reading only the headings of a page conveys what the page is about, in order, without needing the body copy.

Pre-ship hygiene

No automated accessibility violations

blocker a11y

Run axe over every template and fix everything it reports. Treat a clean run as the starting line for the manual checks below, not as evidence of accessibility.

Done when: axe reports zero violations at the WCAG 2.1 A and AA rule sets across the sampled pages, including one page with a form and one with a listing.

Why this rule exists, and how to satisfy it

JavaScript and CSS stay inside their budgets

blocker perf

Assert a gzip budget per chunk in the build and fail the build when it is exceeded. Raising a budget is allowed, in the same commit, with a written note saying what the extra bytes bought.

Done when: The budget test passes, or a raised budget is accompanied by that note in the commit. Without repository access this is reported as unverified, never as a pass.

Ships in wp-starter/bundle-size-test - import it rather than rebuilding it.

CLS at or under 0.1

blocker perf

Reserve space for everything that arrives late: width and height on every image and video, explicit dimensions on embeds and ads, and font fallbacks metric-matched to the web font.

Done when: Median mobile CLS is 0.1 or less, and no visible shift occurs during a slow-network reload watched by eye.

LCP at or under 2.5 seconds on mobile

blocker perf

Identify the LCP element before touching payload. It is usually being discarded rather than loading slowly - an element that finishes loading while hidden is dropped permanently, and opacity zero, clip-path and overflow hidden are all treated the same way. You cannot fade in your largest above-the-fold element and have a fast LCP.

Done when: Median of three or more mobile runs is 2.5s or less, and the identified LCP element is stated in the report alongside the number.

Why this rule exists, and how to satisfy it

HTTPS is enforced and nothing loads over http

blocker seo

Redirect http to https in one hop and make sure no stylesheet, script, image, font or iframe has a hard-coded http:// URL. These usually arrive with migrated content, not code.

Done when: http:// returns a single 301 to https://, and no sampled page contains an http:// asset URL.

Automated check: seo-status:https-redirect, seo-status:mixed-content

Production is indexable and staging is not

blocker seo

Drive the noindex decision from the environment, never from a checkbox someone has to remember. Staging returns noindex on every response; production returns none.

Done when: The production home page carries no noindex in the meta robots or the X-Robots-Tag header, and its robots.txt does not disallow the whole site. Staging carries noindex everywhere.

Automated check: seo-status:home-index, seo-status:robots

Why this rule exists, and how to satisfy it

Replaced URLs redirect to their new home

blocker seo

Only when the project replaces an existing live site.

Export the old site's indexed URLs before launch and map each one to its replacement with a 301. Anything with no equivalent goes to the nearest parent, not to the home page in bulk.

Done when: Every URL on the old-site list returns 200 or a single 301 to a 200. No mass redirect to the home page. Spot-check the twenty highest-traffic URLs by hand.

Why this rule exists, and how to satisfy it

The page survives 200 percent zoom and a 320px viewport

required a11y

Set text-only zoom to 200 percent and check nothing clips or overlaps. At 320 CSS pixels wide there must be no horizontal scrolling of the page body.

Done when: At 200 percent zoom no content is lost or overlapping, and at 320px wide the page scrolls vertically only.

Every template produces valid HTML

required html

Run the W3C validator over a sample covering every template, not just the home page. Unclosed elements and duplicate ids break assistive technology and JavaScript long before they look wrong.

Done when: The validator reports zero errors on the sampled pages. Warnings are read and either fixed or noted with a reason.

Static assets are compressed and cached far-future

required perf

Confirm what the host actually sends rather than assuming. Some hosts compress nothing, in which case every estimate made from a gzipped size is wrong. Fingerprinted assets take a one-year immutable cache header.

Done when: Content-Encoding is br or gzip on HTML, CSS and JS, and hashed assets carry a cache lifetime of a year with immutable.

Why this rule exists, and how to satisfy it

TBT at or under 200ms and INP at or under 200ms

required perf

Keep the main thread free during load. Defer non-critical scripts, break long tasks, and keep third-party tags off the critical path.

Done when: Median mobile TBT is 200ms or less. Where field data exists, INP at the 75th percentile is 200ms or less.

Nothing render-blocking that does not have to be

required perf

Scripts are deferred or type module. Only above-the-fold CSS is render-blocking. No synchronous third-party script in the head. No font stylesheet from another origin blocking first paint.

Done when: The render-blocking resources audit lists nothing beyond the site's own critical stylesheet.

Mobile performance score at or above 90

required perf

Treat the score as a summary of the metrics above, not a target of its own. Chasing the number rather than the metric is how a site ends up fast in a lab and slow for people.

Done when: Median of three or more mobile runs is 90 or above, quoted with the spread of the runs.

Why this rule exists, and how to satisfy it

Production loads with a clean console

required practices

Clear every error and warning, remove debug logging, and make sure no asset 404s. A console full of noise is where the next real error goes unnoticed.

Done when: Loading each sampled page and navigating between two of them produces no console error, no unhandled rejection and no failed network request.

The project ships with the notes needed to run it

required practices

Write down how to build and deploy it, where the environment values live, which plugins are load-bearing, what was deliberately left out, and the indexability decision for filters. One page in the repository, not in a chat thread.

Done when: Someone who has never seen the project can build, run and deploy it from that page alone.

Anything animated or video-driven is checked on a real device

required practices

Open the site on a real iPhone and a real mid-range Android. Simulators do not reproduce Safari's video behaviour, momentum scrolling, or the cost of an animation on a slow GPU.

Done when: Every page with entrance animation or video is opened on a real phone in a fresh private tab, and scrolling stays smooth with video playing.

The layout holds at every width, not just the design widths

required practices

Drag the window slowly from 320 to 2560 and watch for overlap, clipping, orphaned words and horizontal scroll. Designs are drawn at three widths; visitors use every width.

Done when: No horizontal scrollbar at any width from 320 up, no clipped or overlapping content, and no layout that only works at the exact design breakpoints.

Third-party scripts are counted and justified

recommended perf

List every third-party origin the page loads and what it is for. Load analytics and chat after interaction or after load. Anything nobody can name an owner for comes out.

Done when: The third-party summary is reviewed, each origin has a stated purpose, and their combined main-thread time stays under 250ms.

Performance and behaviour changes ship one at a time

recommended practices

Deploy one change, verify it, then start the next. Never widen a change while debugging it. Two changes in one deploy means neither result is attributable.

Done when: Each performance claim in the handover names the single change it came from and the before and after measurement.