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.
- Git repo from commit one — never build outside version control.
-
.gitignorecoversnode_modules/,.env,.DS_Store, build output, and editor cruft. -
A
README.mdin 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.yamlblueprint committed — headers, routes, env vars, and health check live in the repo, not only in the dashboard. -
.nvmrc/enginesfield pinning the Node version. WS -
package-lock.jsoncommitted. 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) orwww— and commit to it. Never both live. - Non-canonical host 301s to canonical.
-
DNS records set:
A/ALIAS/ANAMEfor apex,CNAMEforwww. - TTLs sane — 300s during a cutover, back up to 3600s after.
-
Email DNS untouched by the migration —
MX,SPF,DKIM,DMARCcopied over before any nameserver change. -
A
DMARCrecord 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.
-
Publish directory set correctly (
publicordist). - 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.
-
healthCheckPathset (/healthz) — this is what makes deploys zero-downtime. /healthzreturns 200 fast and touches no database.- Start command uses
node, notnodemon. - Server binds to
process.env.PORTand0.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).
- 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
viewportmeta. -
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:
- charset, viewport
- title
- preconnect to any third-party origin serving render-blocking assets
- critical CSS inline, or the main stylesheet
- font preload for the one or two faces above the fold
- 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/preconnectonly for origins actually used — each one costs a connection. -
No render-blocking JS in the head. Ever.
deferortype="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-widthqueries 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-visiblegets a real ring. -
prefers-reduced-motion: reducehonored — kill transitions for those users. -
prefers-color-schemehandled, orcolor-schemedeclared. -
Anything that animates uses
transform/opacityonly. -
No layout shift: images and embeds carry
width/heightoraspect-ratio. - Minified for production. Unused CSS pruned before launch.
-
content-visibility: autoon long below-fold sections if the page is heavy.
- Default position: the page works with JS off. JS enhances; it doesn't constitute.
-
deferortype="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. Noeval. - 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.
.woff2only.- Subset to the characters actually used — often halves the file.
-
font-display: swap(oroptionalfor non-critical faces). - Preload only the faces above the fold — usually one headline weight, one body weight.
-
Fallback stack declared, with
size-adjusttuned 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+sizesfor anything that scales across breakpoints. -
widthandheighton 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
- Image weight and format
- Font loading strategy
- Third-party scripts — remove one and watch the score jump
- Render-blocking CSS/JS
- 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-Encodingpresent. -
Compression +
etagon; static files served with sane cache options. WS
-
Strict-Transport-Securitywith a long max-age, includeSubDomains, preload. -
Content-Security-Policy— start strict, loosen only where forced. Even adefault-src 'self'baseline beats nothing. X-Content-Type-Options: nosniff.-
X-Frame-Options: DENY(or CSPframe-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.
- HTTPS everywhere; HTTP → HTTPS redirect confirmed.
- No mixed content — zero
http://asset references. - Cert auto-renewal confirmed.
helmetinstalled 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 auditclean, or every finding consciously accepted. Dependabot on.
- 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=andautocomplete=on every field. inputmodeset 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-liveoraria-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
- name, image,
Servicemarkup on each service page.BreadcrumbListif there's a hierarchy.FAQPageon any real FAQ section.Menu/Restaurantfor food clients.Articleon posts, with author and dates.-
AggregateRatingonly 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.txtat the root — allows what should be crawled, points to the sitemap. -
sitemap.xmlat the root: every canonical URL, accuratelastmod, no 404s, no noindex pages. - Sitemap regenerated on build, not hand-maintained.
- Search Console verified, sitemap submitted. Bing Webmaster Tools too.
-
No stray
noindexleft over from staging — the classic launch-day disaster. - Staging/preview URLs are
noindexor password-protected. - Live URL inspected and indexing requested. Coverage report clean after two weeks.
- Clean semantic HTML and real headings — most of what LLM crawlers parse.
- Schema present — the machine-readable facts get lifted into answers.
-
An
llms.txtat 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:andmailto: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 -Ion the live URL — headers are what you think they are. - securityheaders.com grade A. Rich Results schema valid.
-
noindexremoved (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 auditand 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.
- 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
- 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.
Six lines we don't break.
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.
Load it only if it pays rent.
Config, headers, redirects — all in render.yaml.
Especially the contact form.
That's the worst way to find out.
The whole practice, in five words.