Workflows Website best-practices punchlist
Your site looks fine and is quietly broken
A page can render, pass a glance, and still fail a screen reader, leak a header nobody set, or ship live with half its scripts missing and no error anywhere. None of that shows up by looking at it. This is the exact checklist four of my own sites run against before anything ships: what to set, why it matters, and the one-line way to prove it is actually there.
- Covers
- Interface baseline, security headers, deploy safety
- Cost
- $0. CSS, a headers file, and a habit
- Time to apply
- Under an hour on an existing site
- Running on
- 4 live sites, enforced by a script on each one
Interface baseline
Five rules, five minutes, and they belong in the shared stylesheet so every page inherits them instead of remembering them. These lines are pulled straight from a live site's stylesheet, not written up as a sample.
| Rule | Why | Check |
|---|---|---|
color-scheme on html, matching the real theme |
Without it, the browser's own scrollbar and form controls (date pickers, checkboxes) render with light-mode contrast on a dark page. | Toggle dark mode, open a native <select>. Wrong-colored chrome means this is missing. |
<meta name="theme-color"> in every page's head |
This is per page, not per stylesheet, so a second template or a page you forgot is the usual way it goes missing. It sets the phone browser's chrome color. | View source on the page. If the tag isn't in the first few lines of <head>, it's missing. |
A prefers-reduced-motion: reduce block killing animation and transition duration |
Some visitors get physically ill from motion. This is not a nice-to-have, it's the one accessibility rule with a direct medical reason behind it. | Turn on "Reduce motion" in your OS settings, reload the page. Anything still sliding or fading is unguarded. |
touch-action: manipulation and -webkit-tap-highlight-color on every control |
Without the first, mobile Safari waits ~300ms after a tap to see if you meant a double-tap zoom, which reads as a laggy button. The second kills the gray flash on tap that looks like a bug. | Tap a button on a real phone. A delay before it responds, or a gray flash, means one of these is missing. |
A global :focus-visible ring, and a named list of properties on every transition, never the catch-all keyword |
No ring means a keyboard-only visitor can't see where they are on the page. The catch-all keyword silently animates properties you never meant to, including ones a future change adds by accident. | Tab through the page with a keyboard, no mouse. Every interactive element needs a visible outline. |
Copy this
html{color-scheme:light}
html[data-theme="dark"]{color-scheme:dark}
a,button,input,select,textarea,summary,[role=button]{-webkit-tap-highlight-color:transparent}
button,a,summary,[role=button],input[type=submit],input[type=button]{touch-action:manipulation}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
@media (prefers-reduced-motion:reduce){
*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}
}
Adopted from Vercel's Web Interface Guidelines, then turned into a checklist item on 2026-07-22 after an audit found four separate public sites missing the same five things. Add <meta name="theme-color" content="#yourcolor"> to each page's head by hand, it can't live in the CSS.
Security headers
Six headers, set once at the server or host level, no application code involved. Cloudflare Pages syntax below, but every one of these has a direct equivalent in nginx, Netlify, Vercel, or any host that lets you set response headers.
| Header | Why |
|---|---|
X-Content-Type-Options: nosniff | Stops the browser guessing a file's type from its content instead of trusting what the server said, which is how a text upload gets executed as a script. |
X-Frame-Options: DENY | Nobody can put your site in an invisible iframe under their own buttons and trick a click. Cheap, and it costs nothing to have. |
Referrer-Policy: strict-origin-when-cross-origin | Leaving your visitors' full URL path (which can carry a search query or a token) in the referrer header of every outbound link they click. |
Permissions-Policy denying camera, mic, geolocation, USB, payment, etc. | Nothing on a normal content site needs any of these. Denying them means a compromised third-party script embedded on the page can't quietly ask for the camera. |
Strict-Transport-Security, max-age=31536000; includeSubDomains | Tells the browser to never even try the site over plain HTTP again, for a year, closing the one-time downgrade attack that a redirect alone doesn't. |
Content-Security-Policy, default-src 'self', no 'unsafe-inline' scripts | The one that actually stops an injected script from running. Costs the most to add, since every inline <script> and onclick= attribute has to move to a real file first. |
Copy this - the real, live _headers file behind this page
/*
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), autoplay=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), usb=(), interest-cohort=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://www.googletagmanager.com https://*.google-analytics.com; font-src 'self'; connect-src 'self' https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com; upgrade-insecure-requests
The googletagmanager/google-analytics origins are only in there because this site runs GA4. Drop them, and any other third-party origin you don't actually load, down to bare 'self'. Start the CSP in report-only mode (Content-Security-Policy-Report-Only) on a site with any real complexity: it logs violations without blocking anything, so a wrong origin can't take the live site down while you're still finding everything it needs to allow.
Deploy safety
None of the above matters if what ships isn't what you meant to ship. These are the rules that catch that, all of them written after something actually went wrong.
| Rule | Why |
|---|---|
| Deploy an explicitly enumerated set of files, never a working directory | A folder is whatever happens to be sitting in it, including draft mockups, old experiments and anything else that landed there since you last looked. One deploy shipped a set of design mockups straight to production this way. |
| Build from your last commit, not your working tree | An uncommitted change never ships, and a change spread across two files where only one got committed ships half-finished, silently, with no error. |
| After deploying, probe the live endpoint, not a file listing | A deploy can serve every static asset correctly and drop the backend functions entirely, and still report success. One incident here cost 16 hours 37 minutes: two deploys said "success," a form quietly returned an error page to every visitor, and nothing paged anyone because nothing failed loudly. |
| Retry the probe a few times before trusting a failure | CDN propagation isn't instant. A single check right after deploying can hit an edge node still serving the previous version, which looks exactly like a broken deploy and isn't one. |
The common thread: "the deploy tool said success" is not evidence. Only a real request to the live URL, checked more than once, is.
Take it
Nothing here needs a library, a build step, or a service to sign up for. Copy the CSS, copy the headers file, and add the two deploy habits to whatever ships this site.
- Build a settings file for Claude Code If an agent is doing the shipping, this is where a check like "probe the live endpoint" gets wired in so it runs on its own.
- Make it check its own work The companion habit: nothing above catches a change that was simply wrong, only one that shipped wrong.
- Every Claude Code hook, explained Reference for wiring any of this into a pipeline that runs without you.
Don't check this by hand
The five interface rules above are enforced by a script I keep public and MIT licensed. Point it at your built output rather than your source, which is the distinction that matters: a search over my source said all four sites were clean and they were not. It exits red the moment one of the five goes missing, so a build step can fail on it. The headers and the deploy habits are still yours to wire up.
Get the checker →