Build standards

The bones behind every site.

Everything that has to be true underneath a site for it to be fast, findable, secure, and still standing in five years. Not a style guide — a structure guide.

Two stacks, one standard: WSNode + Express web service SSstatic site — everything else applies to both.
  • Git repo from commit one — never build outside version control.
  • .gitignore covers node_modules/, .env, .DS_Store, build output, and editor cruft.
  • A README.md in every repo: what it is, how to run it locally, how to deploy, and where the DNS lives.
  • .editorconfig — 2-space indent, LF line endings, final newline, trim trailing whitespace.
  • Predictable folder layout, the same in every project:
/public          → deployable root (SS: publish dir)
  /assets/css
  /assets/js
  /assets/img
  /assets/fonts
  favicon.ico
  robots.txt
  sitemap.xml
/src             → [WS] server code
render.yaml      → infra as code
  • render.yaml blueprint committed — headers, routes, env vars, and health check live in the repo, not only in the dashboard.
  • .nvmrc / engines field pinning the Node version. WS
  • package-lock.json committed. WS
  • Main branch is what deploys. Work on branches. PR previews on for anything nontrivial.
  • Semantic commit messages — you will be reading these in two years.
  • Domain registered to the client's account where possible — we manage, they own.
  • Registrar auto-renew on. Expiry date written down.
  • Domain privacy / WHOIS protection on.
  • Registrar lock on.
  • Pick the canonical host — apex (example.com) or www — and commit to it. Never both live.
  • Non-canonical host 301s to canonical.
  • DNS records set: A/ALIAS/ANAME for apex, CNAME for www.
  • TTLs sane — 300s during a cutover, back up to 3600s after.
  • Email DNS untouched by the migration — MX, SPF, DKIM, DMARC copied over before any nameserver change.
  • A DMARC record exists at minimum (v=DMARC1; p=none; rua=…) so nobody spoofs the client.
  • Old host's DNS left running until the new one is confirmed live.
Static sites SS
  • Publish directory set correctly (public or dist).
  • Build command set, or skipped only if there's genuinely nothing to build.
  • Auto-deploy on push to main.
  • Custom domain added; TLS cert issued automatically, free and auto-renewing.
  • Brotli compression and HTTP/2 confirmed active.
  • Redirects/rewrites defined in render.yaml, not hand-rolled JS.
  • Custom headers defined in render.yaml.
  • PR previews enabled.
Web services WS
  • healthCheckPath set (/healthz) — this is what makes deploys zero-downtime.
  • /healthz returns 200 fast and touches no database.
  • Start command uses node, not nodemon.
  • Server binds to process.env.PORT and 0.0.0.0.
  • NODE_ENV=production.
  • Secrets in env vars — never in the repo.
  • Graceful shutdown on SIGTERM — close server, drain, exit.
  • Structured logging to stdout as JSON, not console.log("here").
  • Region chosen closest to the audience (Ohio for Minnesota).
Both
  • Deploy is one push. If it takes a manual step, it gets written down or automated.
  • Rollback path known and tested once before launch.
  • <!DOCTYPE html> and <html lang="en">.
  • <meta charset="utf-8"> first thing in the head.
  • A responsive viewport meta.
  • One <h1> per page. Heading levels never skip (h1 → h2 → h3).
  • Real semantic elements: header, nav, main, article, section, footer.
  • Exactly one <main> per page.
  • Buttons are <button>. Links are <a href>. Never a <div onclick>.
  • Lists are lists. Tables are tables, with <th scope> and a caption.
  • Every page has a unique <title>, ~55–60 characters.
  • Every page has a unique meta description, ~150–160 characters, written by a human.
  • Self-referencing <link rel="canonical"> on every page, absolute URL.
  • Open Graph: title, description, image (1200×630), url, type, site name. Twitter card set.
  • No inline style= attributes in production markup.
  • HTML validates.
  • Content is in the HTML source. If JS has to run for text to exist, we've made a problem.

Order in the <head> dictates how fast the browser starts work:

  1. charset, viewport
  2. title
  3. preconnect to any third-party origin serving render-blocking assets
  4. critical CSS inline, or the main stylesheet
  5. font preload for the one or two faces above the fold
  6. everything else — meta, Open Graph, schema, deferred JS
  • Preload the LCP image if it's a hero (rel="preload" as="image" fetchpriority="high").
  • dns-prefetch / preconnect only for origins actually used — each one costs a connection.
  • No render-blocking JS in the head. Ever. defer or type="module".
  • A single stylesheet, or a small ordered handful. No framework unless it earns its weight.
  • Modern reset at the top — border-box, margin zeroing, responsive images.
  • Design tokens as CSS custom properties in :root — color, spacing, type scale, radii.
  • Mobile-first: base styles are the phone; min-width queries add up from there.
  • Fluid type with clamp() where it helps; no fixed-pixel body text.
  • Layout is Grid/Flex. No float hacks, no absolute-positioning a whole page.
  • Focus outlines never suppressed without replacement — :focus-visible gets a real ring.
  • prefers-reduced-motion: reduce honored — kill transitions for those users.
  • prefers-color-scheme handled, or color-scheme declared.
  • Anything that animates uses transform/opacity only.
  • No layout shift: images and embeds carry width/height or aspect-ratio.
  • Minified for production. Unused CSS pruned before launch.
  • content-visibility: auto on long below-fold sections if the page is heavy.
  • Default position: the page works with JS off. JS enhances; it doesn't constitute.
  • defer or type="module" — never a bare blocking script in the head.
  • Zero dependencies unless one is genuinely cheaper than writing it.
  • No jQuery. It's 2026.
  • Event listeners delegated where sensible; removed when elements are torn down.
  • Web Components for anything reused across pages — framework-free, no build step.
  • Nothing renders text that could have been HTML.
  • Third-party scripts audited one by one. Each is a tax on speed, privacy, and security.
  • No document.write. No eval.
  • Errors caught and, if it matters, reported — not swallowed.
  • Minified for production. Total JS budget under 100 KB compressed for a brochure site.
  • Self-hosted, not fetched from a third party at runtime — faster and private.
  • .woff2 only.
  • Subset to the characters actually used — often halves the file.
  • font-display: swap (or optional for non-critical faces).
  • Preload only the faces above the fold — usually one headline weight, one body weight.
  • Fallback stack declared, with size-adjust tuned if the swap causes a jump.
  • Two families, three weights max, unless there's a reason.
  • Every image resized to its actual maximum display size before it's ever compressed.
  • Modern formats: AVIF first, WebP fallback, JPEG/PNG last — via <picture>.
  • srcset + sizes for anything that scales across breakpoints.
  • width and height on every image — the single biggest CLS fix.
  • loading="lazy" on everything below the fold.
  • fetchpriority="high" on the LCP image, and never lazy-load it.
  • decoding="async" on non-critical images.
  • Alt text on every image; decorative images get alt="".
  • SVGs for logos and line art, optimized through SVGO, no editor metadata.
  • Full favicon set plus a site.webmanifest.
  • Video never autoplays with sound; poster image or preload="none".
  • No image over ~200 KB without a very good reason. Hero images under 150 KB.
  • Originals archived — we'll want them for the yearly overhaul.

Core Web Vitals — field data, 75th percentile, mobile:

  • LCP ≤ 2.5 s.
  • INP ≤ 200 ms.
  • CLS ≤ 0.1.
  • TTFB ≤ 800 ms.
  • Lighthouse mobile Performance ≥ 90 — a smoke test, not the goal.
  • Homepage first load under 500 KB compressed. Aim lower.
  • Under ~30 requests on first load.
  • Tested on throttled 4G, mid-tier Android — not a laptop on fiber.
  • Tested on the real deployed URL, not localhost.

Levers, in order of payoff

  1. Image weight and format
  2. Font loading strategy
  3. Third-party scripts — remove one and watch the score jump
  4. Render-blocking CSS/JS
  5. Caching headers
  • Hashed asset filenames (main.a3f9c1.css) so they cache forever and still ship changes.
  • Headers set in render.yaml:
headers:
  - path: /assets/*
    name: Cache-Control
    value: public, max-age=31536000, immutable
  - path: /*.html
    name: Cache-Control
    value: public, max-age=0, must-revalidate
  • HTML always revalidates; fingerprinted assets never do.
  • Non-fingerprinted assets capped at something sane (max-age=3600) — an immutable header on a mutable file will haunt you.
  • Verified with curl -I.
  • Brotli confirmed on. Vary: Accept-Encoding present.
  • Compression + etag on; static files served with sane cache options. WS
Headers
  • Strict-Transport-Security with a long max-age, includeSubDomains, preload.
  • Content-Security-Policy — start strict, loosen only where forced. Even a default-src 'self' baseline beats nothing.
  • X-Content-Type-Options: nosniff.
  • X-Frame-Options: DENY (or CSP frame-ancestors 'none').
  • Referrer-Policy: strict-origin-when-cross-origin.
  • Permissions-Policy — turn off geolocation, camera, microphone you don't use.
  • Graded A or better at securityheaders.com.
Transport
  • HTTPS everywhere; HTTP → HTTPS redirect confirmed.
  • No mixed content — zero http:// asset references.
  • Cert auto-renewal confirmed.
Application WS
  • helmet installed and configured.
  • Rate limiting on every public POST endpoint.
  • Body size limits (express.json({ limit: '10kb' })).
  • All input validated server-side against a schema — client validation is a courtesy, not a control.
  • All output escaped. Parameterized queries only — never string-concatenated SQL.
  • CORS locked to known origins, not *.
  • Cookies: httpOnly, secure, sameSite=lax, signed.
  • CSRF protection on state-changing form posts.
  • npm audit clean, or every finding consciously accepted. Dependabot on.
Both
  • 2FA on the domain registrar, host, Google, and Stripe.
  • Secrets rotated if ever committed, exposed, or shared over an untrusted channel.
  • A real <label> bound to every input.
  • Correct type= and autocomplete= on every field.
  • inputmode set where it helps a phone keyboard.
  • Native validation with a visible error state that isn't color-only.
  • Server-side validation regardless of what the client did.
  • Honeypot field, hidden, must stay empty — catches most bots for free.
  • Timestamp check — submitted in under two seconds means a bot.
  • Rate limit per IP. A CAPTCHA only if the honeypot fails.
  • Submission goes somewhere a human sees it, tested from a phone on cellular.
  • Sender is a domain address with SPF/DKIM aligned — not noreply@gmail.
  • Autoresponder confirms receipt to the customer.
  • A thank-you state — never a blank screen. Errors don't wipe what they typed.
  • A monthly test submission is on the maintenance calendar.
  • Keyboard-only pass: tab through the whole site, do everything, get stuck nowhere.
  • Visible focus ring on every interactive element.
  • A "skip to content" link as the first focusable element.
  • Contrast: 4.5:1 body text, 3:1 large text and UI borders — including the muted variants, not just the primary.
  • Nothing communicated by color alone.
  • Touch targets at least 44×44 px.
  • Landmarks present — screen readers navigate by them.
  • ARIA only where semantic HTML can't do the job. Bad ARIA is worse than none.
  • Forms announce their errors (aria-live or aria-describedby).
  • Zoom to 200% — nothing breaks, nothing is cut off.
  • axe DevTools or Lighthouse a11y clean of critical issues before launch.
  • Read one page with a screen reader at least once. It changes how you build.
  • Unique title and meta description per page.
  • One <h1> per page, describing what the page is.
  • URLs lowercase, hyphenated, word-based, shallow — /services/panel-upgrades.
  • A trailing-slash convention picked and enforced with a redirect.
  • One page, one job. No page trying to rank for four services.
  • Town and region named naturally in headings and copy.
  • Internal links with descriptive anchor text — never "click here".
  • No orphan pages — everything reachable from navigation or a contextual link.
  • No duplicate or near-duplicate pages. Canonicals correct.
  • A custom 404 that helps — links back home and to the main services.
  • Every 301 from an old URL structure mapped and tested. Break no inbound link.
  • Text is real text, not baked into an image.
  • Content is genuinely useful. Everything else is scaffolding for this line.
  • JSON-LD in a <script type="application/ld+json"> — not microdata.
  • LocalBusiness (or the correct subtype) on the homepage:
    • name, image, @id, url, telephone
    • address, geo, opening hours, priceRange
    • sameAs — the social profiles
  • Service markup on each service page.
  • BreadcrumbList if there's a hierarchy.
  • FAQPage on any real FAQ section.
  • Menu / Restaurant for food clients.
  • Article on posts, with author and dates.
  • AggregateRating only if the reviews are real and on-site. Faking it is a manual penalty.
  • Name, address, and phone in schema match the Business Profile and footer exactly.
  • Validated in the Rich Results Test. Zero errors.
  • robots.txt at the root — allows what should be crawled, points to the sitemap.
  • sitemap.xml at the root: every canonical URL, accurate lastmod, no 404s, no noindex pages.
  • Sitemap regenerated on build, not hand-maintained.
  • Search Console verified, sitemap submitted. Bing Webmaster Tools too.
  • No stray noindex left over from staging — the classic launch-day disaster.
  • Staging/preview URLs are noindex or password-protected.
  • Live URL inspected and indexing requested. Coverage report clean after two weeks.
AI search visibility
  • Clean semantic HTML and real headings — most of what LLM crawlers parse.
  • Schema present — the machine-readable facts get lifted into answers.
  • An llms.txt at the root: a plain-language map of the business and its key pages.
  • Decide with the client whether to allow GPTBot, ClaudeBot, PerplexityBot. For a local business, almost always yes — being cited is free distribution.
  • Google Business Profile claimed, verified, every field filled.
  • Correct primary category plus secondary categories.
  • Service area or address set correctly. Hours, including holidays.
  • Real photos, refreshed periodically. Products and services listed.
  • Website link points to the canonical URL.
  • Bing Places and Apple Business Connect claimed (Apple Maps is not nothing up here).
  • Name, address, phone consistent across site, Business Profile, Bing, Apple, Facebook, and directories.
  • Local citations: the Bemidji Area Chamber, regional tourism, trade associations.
  • A review flow set up — One-Tap Review cards, a short link, and a plan for actually asking.
  • Client shown how to reply to reviews, and told to reply to all of them.
  • Analytics installed and privacy-light — Plausible or Fathom over GA4 unless the client needs GA4.
  • If GA4: consent handled, IP anonymization, data retention set.
  • A consent banner only if you actually set cookies that require one. No friction you don't owe.
  • Key events tracked: form submit, phone tap, directions tap, booking start and complete.
  • tel: and mailto: links tracked as conversions.
  • Search Console linked to analytics.
  • Real-user Core Web Vitals collected.
  • Baseline numbers recorded at launch, so the monthly report has something to compare to.
  • Uptime monitor on the homepage — 1–5 minute interval, alerts to a phone.
  • A second monitor on a deep page or an API endpoint, not just /.
  • SSL expiry monitoring — belt and suspenders.
  • Domain expiry reminder on the calendar, 60 days out.
  • Error tracking with alerts. Log retention and a way to search them. WS
  • Broken-link check scheduled, not ad hoc.
  • Backups: the repo is the backup for a static site; database backups automated and restore-tested once. WS
  • A restore actually performed at least once. An untested backup is a rumor.
  • An incident plan: what the client is told, how fast, and by whom.
  • Every link clicked. Zero 404s.
  • Every form submitted, from a phone, on cellular. Confirmed it landed.
  • Every phone number tapped — it dials. Every address tapped — it opens maps.
  • Every page read aloud. Whatever makes you stumble gets fixed.
  • Hours, prices, dates, staff names — all currently true.
  • Tested on real iPhone Safari, real Android Chrome, desktop Chrome, Safari, and Firefox.
  • Tested at 320 px wide — nothing overflows horizontally.
  • Lighthouse Performance, Accessibility, Best Practices, SEO all ≥ 90 mobile.
  • curl -I on the live URL — headers are what you think they are.
  • securityheaders.com grade A. Rich Results schema valid.
  • noindex removed (say it out loud). 404 page works and is styled.
  • Favicon appears in the tab. Open Graph preview checked in a real paste.
  • Print stylesheet doesn't produce a disaster. Spellcheck — then someone else spellchecks.
  • Deploy during a low-traffic window. DNS cut over with TTLs already lowered.
  • Padlock holds on the live custom domain, not just the host's URL.
  • The www ↔ apex redirect fires the right direction.
  • HTTP → HTTPS redirect confirmed.
  • All old URLs 301 to their new homes — spot-check ten.
  • Sitemap submitted; indexing requested for the homepage.
  • Business Profile website link updated.
  • Uptime monitor turned on before anyone is told.
  • Analytics firing — checked in real time.
  • Client walked through the live site on a call or in person. Not just a link.
  • Launch-day Lighthouse and Core Web Vitals screenshotted for the record.
  • Every account and login documented and handed over securely.
  • Client is owner on the domain, Business Profile, and analytics; we hold admin.
  • A one-page "what you can change yourself" doc.
  • What the monthly plan includes, written down plainly, so there's never a misunderstanding.
  • Code-ownership terms restated: the client owns the site code after the three-month minimum, paid in full; Verdant Dev keeps its reusable tooling and hosted services.
  • Repo and deploy documented well enough that someone else could pick it up — including us, in two years.

Every client, every month — this is the plan they're paying for.

  • Content updates requested by the client, applied.
  • Hours, prices, and seasonal info verified still true.
  • Uptime report pulled — target ≥ 99.9%.
  • Core Web Vitals checked; any regression investigated.
  • Search Console: coverage errors, manual actions, security issues — all clear.
  • Search Console: impressions, clicks, top queries, position changes.
  • Backlink profile checked. Broken-link scan run.
  • Contact form test submission.
  • Business Profile: posts, photos, Q&A, review replies current.
  • New reviews landed or requested.
  • npm audit and dependency updates applied and deployed. WS
  • Error log skimmed for anything recurring. WS
  • Backups verified.
  • A monthly health report written and sent — plain language, one page.
Quarterly
  • Full content review with the client — anything stale, anything new to say.
  • Full accessibility re-scan. Full Lighthouse re-run on the top three pages.
  • A competitor glance — what's changed in their search results.
  • Query review: what are people actually finding you for? Write toward it.
  • Local citations re-verified — they drift.
  • Node LTS and dependency major-version review. WS
Yearly
  • The style overhaul we promise.
  • Full technical re-audit against this checklist, top to bottom.
  • Domain renewal confirmed. Schema re-validated against current spec.
  • Image library re-compressed with whatever format has arrived since.
  • Analytics year-over-year written up for the client.
  • Ask: does the site still say what this business is? Businesses change quietly.
Standing principles

Six lines we don't break.

The bones show.

A site with clean semantics, honest content, and a small payload will outrank a prettier one with none of that — and it will still be working in five years.

Every third-party script is a debt.

Load it only if it pays rent.

If it isn't in the repo, it doesn't exist.

Config, headers, redirects — all in render.yaml.

If it isn't tested, it's broken.

Especially the contact form.

If it isn't monitored, you'll find out from the client.

That's the worst way to find out.

Slower to make, longer to last.

The whole practice, in five words.

© MMXXVI · Made carefullyBemidji, MN