jared hebb ~ %

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.

RuleWhyCheck
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.

HeaderWhy
X-Content-Type-Options: nosniffStops 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: DENYNobody 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-originLeaving 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; includeSubDomainsTells 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' scriptsThe 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.

RuleWhy
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.

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 →