The stack
Every site I ship runs on the same foundation. I pick tools that are fast, maintainable, boring in the best sense, and that do not require me to babysit a runtime. Versions and lockfiles are pinned in every repo; you can audit exactly what's running by reading package.json.
Core
- Eleventy (11ty) v3 ESM: the static-site generator. Converts Nunjucks templates, Markdown content, and JavaScript data files into flat HTML at build time. The config (
eleventy.config.js) is ESM with"type": "module"inpackage.json. - Nunjucks: templating. Supports inheritance, partials, filters, and JavaScript data. I compose layouts via
extendsand partials viainclude; no monolithic templates. - Vanilla CSS with custom properties,
clamp(),@containerqueries where appropriate, and zero framework. One stylesheet, concatenated at build time from organized partials. No@importin production (it causes request waterfalls). - Vanilla JavaScript: no framework, no build step for JS, no hydration. Progressive enhancement only. The site works fully without JS; JS exists to make a few interactions nicer.
- Markdown for blog posts and long-form content via 11ty's built-in markdown-it pipeline. Front-matter for metadata. Content is plain text in
.mdfiles; nothing is locked in a database.
Build-time plugins
- @11ty/eleventy-img: generates AVIF, WebP, and JPEG variants with width-based
srcset, native lazy loading, and dimension hints to prevent CLS. - @11ty/eleventy-plugin-rss: RSS / Atom feed generation for the blog.
- @sardine/eleventy-plugin-tinyhtml: HTML minification with conservative whitespace handling.
- eleventy-plugin-gen-favicons: full favicon set (every device, every browser, every dark/light variant) generated from a single source.
- eleventy-auto-cache-buster: appends content-hashed query strings to CSS/JS references so returning visitors get fresh assets without serving stale ones.
- Custom CSS-concatenation extension: registers
.cssas a template format ineleventy.config.js, reads partial files in declared order, and emits one minified stylesheet. I never ship@importto the browser. - Custom sitemap template:
sitemap.njkiteratescollections.all, filters out assets and excluded pages, and emits a validsitemap.xmlwithpriorityandchangefreq. I do not use@quasibit/eleventy-plugin-sitemapor@11ty/eleventy-plugin-sitemap; both have reliability issues with v3.
Functional services (runtime, third-party)
- Pagefind: static, client-side full-text search. Indexed at build time, served as static JSON chunks, no server required. Runs in an
eleventy.afterhook so the Cloudflare Pages preset works out of the box. - Web3Forms: static-site-friendly form backend. Submissions POST directly to their API; I never touch a server-side form handler. Honeypot + rate-limiting; no CAPTCHA.
- Tippy.js + Popper: accessible tooltip library for glossary terms. Initialized site-wide; auto-attaches to
[data-term],[data-tooltip], and any link to the glossary so authors can write a normal anchor and get a definition tooltip free. - Leaflet + OpenStreetMap: service-area maps on
/areas/<slug>/pages and demo sites. No Google Maps tracker, no API key, no cookies. Tiles served directly from OSM.~200city coordinates curated in_data/cityCoords.jsfor nearby-community pin plotting; ambiguous names (Aurora CO/IL, Smyrna GA/TN) are state-scoped. - instant.page: prefetches links on hover so navigation feels instantaneous. 1.5 KB, no API.
Analytics
- Umami: open-source, cookie-free analytics. Primary traffic data. Included with every monthly plan; I host the dashboard and send each client a private live link on launch day.
- Cloudflare Web Analytics: server-side Real User Monitoring (RUM) for Core Web Vitals. Zero client-side tracking, no cookies, GDPR-compliant by design.
Hosting & DNS
- Cloudflare Pages: static hosting with global Anycast CDN, automatic HTTPS via free SSL, HTTP/3 by default, unlimited bandwidth on the Free tier. Build container runs
npx @11ty/eleventyon every push tomain. - Cloudflare Workers (when needed): serverless edge functions for routing or simple APIs. I use them sparingly: only when Web3Forms can't cover a need.
- DNS: Cloudflare nameservers for clients I host fully; for clients who prefer to self-manage at their existing registrar, I provide the exact records to add.
- Your domain stays in your name. I never own a client's domain. I do not register domains on a client's behalf because it creates the wrong control structure on the wrong day.
Source control & deploy
- GitHub: every client site lives in a private repo. Commit history available to the client on request. I sign commits with verified GPG keys.
- CI/CD: Cloudflare Pages' built-in build preset runs
npx @11ty/eleventyon every push tomain. Preview deploys for every non-mainbranch. Roll-backs in one click. I do not maintain a separate CI server. - Branch protection:
mainis protected; merges require status checks (build + Lighthouse CI on critical templates).
Interactive tooling I have shipped on this site
The agency's own site doubles as a portfolio of small interactive surfaces. Each is fully client-side: nothing leaves the browser, nothing depends on a backend, all of it works on a static-site host.
- The lead-leakage calculator. Six inputs (visits, mobile share, current PageSpeed, conversion rate, average job value, close rate), four outputs (annual revenue at risk, lost mobile visits/month, additional leads/year, additional jobs). Pure vanilla JS, runs entirely in the browser, no submission anywhere. Pairs with the long-form math blog post.
- The free 5-point audit form. Real Lighthouse + schema + conversion-flow review delivered on a 5-business-day SLA. Web3Forms backend, unique subject line for routing.
- The 8-page comparison surface. Honest head-to-head vs Wix, Squarespace, GoDaddy, Webflow, WordPress, AI builders, traditional agencies, and freelance developers. Each page leads with the lead-generation gap (page speed, schema, form intelligence) instead of the price line, since the agency is not the cheapest option but is the highest-leverage option for service businesses.
- The walk-through page. Sales-call-free evaluation surface for visitors who want to see the work before booking a call.
- The redesigned client portal. Single-page dashboard for active clients with quick-action shortcuts, stage-aware feature cards, integrated direct-contact panel, and clean self-serve resource list.
Why static over dynamic
For the kind of sites I build (small-business marketing, service-area pages, blogs), there's no technical reason to be running a database, a PHP runtime, or a Node server on the critical path of a page request. Every layer of runtime infrastructure I remove is a layer that can't break, can't be hacked, and can't introduce latency.
WordPress
WordPress powers ~40% of the web, runs on PHP + MySQL, and is the single most-attacked web platform by a large margin. Every plugin adds attack surface. Every missed update is a vulnerability. Core Web Vitals are a constant fight because of bloated themes, render-blocking scripts, and unoptimized database queries. Even well-tuned WordPress sites struggle to hit the Good threshold without aggressive caching plugins and a page builder that undoes half the optimization.
I do not build on WordPress because the upside (a familiar admin panel) doesn't outweigh the downsides (ongoing maintenance, recurring vulnerabilities, performance ceilings, runtime dependence on a PHP host). Clients do not miss the admin panel once they realize they can just email me what they want changed.
Next.js, Astro, SvelteKit, Remix
Modern meta-frameworks are excellent tools for application-shaped problems: apps with user accounts, real-time state, interactive dashboards, e-commerce with live inventory. For a six-to-twelve-page service-business website they are overkill. They add a JavaScript runtime that the visitor never sees the benefit of, they shift complexity onto the developer (and therefore onto the recurring bill), and they require a Node server or a serverless platform on the hot path, which introduces cold starts, cost variability, and another thing to monitor.
Astro is the closest analogue to what I would reach for if 11ty didn't exist, and its island architecture is elegant. I stay on 11ty because the sites I build don't need islands; they need clean HTML and CSS served from a CDN with sub-100ms TTFB, and 11ty ships exactly that with less abstraction.
Squarespace, Wix, Webflow, Framer
Hosted builders are great for absolute beginners and for businesses that want a website but never plan to grow one. The tradeoff is you are locked into a proprietary platform, you pay $16 to $50 per month forever just to keep the lights on, and your SEO is capped by the platform's decisions. Switching away typically means rebuilding. I would rather my clients own the site outright (or lease it from me with an always-available buyout) than build on rented infrastructure.
Performance targets & methods
I target the Good threshold on every Core Web Vital for both mobile and desktop, on every page of every site I ship. Specifically:
- Largest Contentful Paint (LCP): < 1.0s (Google's Good threshold is < 2.5s).
- Cumulative Layout Shift (CLS): < 0.05 (Good: < 0.1).
- Interaction to Next Paint (INP): < 100ms (Good: < 200ms).
- Total Blocking Time (TBT): < 100ms.
- First Contentful Paint (FCP): < 800ms.
- Time to First Byte (TTFB): < 100ms from any North-American visitor.
- Google PageSpeed score: 95 to 100 on mobile and desktop.
Performance budget
I enforce a per-page asset budget. If a deploy would exceed it, the build fails until I either trim or justify.
- HTML: under 25 KB gzipped per page.
- CSS: one stylesheet, under 30 KB gzipped, shared across the entire site.
- JavaScript: under 10 KB gzipped per page (typical pages ship 4 to 7 KB).
- Hero image: under 80 KB AVIF / 120 KB WebP at the responsive width that visitor needs.
- Web fonts: at most two faces total; subset to Latin only unless content requires more.
- Third-party scripts: zero by default. Each addition requires a documented reason and a budget line.
How I hit those numbers
- Zero render-blocking JavaScript. All scripts use
deferor aretype="module". I ship under 10 KB of gzipped JS on a typical page. - Responsive images with AVIF + WebP fallback. Every image has explicit width and height attributes to prevent layout shift, native
loading="lazy"for below-the-fold content, anddecoding="async"for progressive rendering. - System font fallback stack. Bunny Fonts preconnect +
font-display: swapmeans the page renders in a system font if custom fonts take >100ms, eliminating flash of invisible text. - CSS concatenated at build. One stylesheet, one HTTP request. No
@importcascading requests. Inline cache-busting via query string based on content hash (?v=4a701c154dc9). - Minified HTML, CSS, JS. Production builds run through
html-minifier-terserwithminifyCSSandminifyJSenabled. - Cloudflare's edge. Static files served from the CDN closest to the visitor. HTTP/3, Brotli compression, and Early Hints where supported.
- No web fonts on first-line content. The hero uses system fonts as a fallback so the first paint always has readable text.
- Preconnect to font origin. A single
preconnecthint tofonts.bunny.netin<head>shaves ~200 ms off font-fetch on cold loads. - Per-page critical CSS. Because the stylesheet is small enough to ship in full (under 30 KB gzipped), I do not bother with critical CSS extraction. The whole sheet arrives before render.
Image pipeline
Images are the single largest performance lever on a typical small-business site. The pipeline is non-negotiable.
- Source format: I accept anything the client sends (HEIC, RAW, JPEG, PNG). The build pipeline normalizes to JPEG internally and emits AVIF + WebP + JPEG.
- Output formats: AVIF first, WebP fallback, JPEG floor.
<picture>with<source type="image/avif">and<source type="image/webp">,<img>as the JPEG floor. - Responsive widths: 320, 480, 640, 800, 1200, 1600, 2000 px. Each image gets a
srcsetwith at least four widths; the browser picks the right one based on viewport, DPR, and bandwidth hints. - Layout stability: every
<img>has explicitwidthandheightattributes (the source dimensions). Browsers use these to reserve space, eliminating layout shift. - Lazy loading:
loading="lazy"on all below-the-fold images,decoding="async"on everything. The hero image is eager and getsfetchpriority="high". - Quality targets: AVIF at q=70, WebP at q=80, JPEG at q=82. These thresholds were tuned by side-by-side comparison; below them, banding becomes visible on photographs.
- Naming: filenames are content-hashed so cache busting is automatic. A given source produces deterministic output names; the same source compiled twice produces the same files.
- Total reduction: a 4 MB raw photo from a phone typically becomes a 60 KB AVIF / 90 KB WebP / 140 KB JPEG at the largest delivered size, with smaller siblings down to ~12 KB for mobile.
Font strategy
Web fonts are a performance trap. The default behavior (FOIT or FOUT) is bad enough that I treat font loading as a first-class problem.
- Provider: Bunny Fonts. GDPR-compliant Google Fonts mirror, no cookies, no tracking, no consent banner required.
- Preconnect:
<link rel="preconnect" href="https://fonts.bunny.net">in<head>. This opens the connection before the CSS asks for the fonts. - Display strategy:
font-display: swap. The browser renders text in the system fallback immediately, then swaps in the web font when it arrives. - Subsetting: Latin only by default. Adding more subsets is one line in the URL but doubles the file size; I only do it if content requires it.
- Face count: two families maximum (display + body), with at most three weights each. Variable fonts when the family supports them.
- System fallback stack: every font family declaration ends in a real system fallback (
system-ui, -apple-system, "Segoe UI", Roboto, sans-serif) so the first paint is always readable. - Cache: Bunny serves with a year-long cache. Returning visitors don't re-download.
CSS architecture
One stylesheet, vanilla CSS, custom properties, partials concatenated at build. No framework, no preprocessor, no CSS-in-JS, no PostCSS. Just CSS.
- Partials live in
src/assets/css/organized by concern:reset.css,tokens.css,utilities.css,components/*.css,pages/*.css. Each file owns its concern and nothing else. - The entry point (
global.css) is mostly a comment. A custom Eleventy template extension reads each partial in declared order withfs.readFileSyncand emits a single concatenated, minified file. - Why no
@import: the browser blocks rendering on every@importchain, creating a request waterfall. Concatenation at build time avoids this entirely. - Custom properties everywhere. Color, type scale, spacing scale, radius, shadow, transition, breakpoint variables are all in
tokens.css. Themes (light/dark) flip the values, not the components. - Fluid type and spacing via
clamp(). Most type tokens areclamp(min, preferred, max)so they scale with viewport without media-query breakpoints. - Container queries for components that need to respond to their parent rather than the viewport. Mostly used for cards in flexible grids.
- Cascade layers (
@layer reset, tokens, utilities, components, pages) make specificity predictable. Late-loaded utilities never accidentally outrank components. - Cache busting: the stylesheet ships with a content-hash query string (
style.css?v=4a701c154dc9) so a deploy invalidates the cache for returning visitors atomically.
JavaScript architecture
JavaScript exists to make a few interactions nicer. The site works fully without it. Specifically:
- No framework, no router, no state library. Every script is a small, self-contained module.
- Defer + module type. All scripts are loaded with
deferor astype="module"so they never block parsing. - Inline no-flash theme script in
<head>. ReadslocalStorage+prefers-color-schemeand setsdata-themeon<html>before paint. Prevents the dark-to-light flash. - Inline event handlers as a fallback for hamburger toggles, search opens, and theme toggles. The minifier can occasionally break event-listener binding for deferred scripts; inline
onclickattributes act as a belt-and-suspenders safety net. - IntersectionObserver for blog TOC: the sticky table of contents updates the active section as the visitor scrolls. No scroll-event listeners.
- Pagefind UI: bundled by Pagefind itself as a small ESM module. Loaded only on pages with a
#searchcontainer. - Reading progress bar, copy-link buttons, social share on blog posts. All vanilla, all under 2 KB combined.
- No tracking pixels. No third-party advertising scripts. Analytics is server-side or first-party.
Theming & dark mode
Light/dark themes are first-class. Theme tokens live in tokens.css as CSS custom properties; :root[data-theme="dark"] overrides the values. Components reference tokens, never raw colors.
- Toggle behavior: three states (Light, System, Dark) via a segmented control. System tracks
prefers-color-schemelive. - Persistence: the user's choice is written to
localStorage. The next visit applies it before paint. - No flash: a tiny inline script in
<head>reads the stored choice and setsdata-themebefore the body renders. Without this, the page momentarily renders in the wrong theme. - Reduced motion: a global rule disables transitions and animations when
prefers-reduced-motion: reduceis set. I test this with the OS-level setting enabled. - Forced colors: I test in Windows High Contrast mode; system colors are honored where appropriate via
forced-colors: active. - Color-scheme meta:
<meta name="color-scheme" content="light dark">tells the browser the intent so form controls and scrollbars adapt automatically.
Site search
Pagefind generates a static index at build time. Search runs entirely in the visitor's browser with no server, no API, no cookies.
- Index size: typically 50 to 250 KB total for a small-business site, served as small JSON chunks fetched on demand.
- Indexed content: every page in
collections.allminus excluded paths (style guide, brand guidelines, sitemap, etc.). Front-mattertagsbecome filters when I wire them. - Build hook: Pagefind runs in an
eleventy.afterhook so Cloudflare Pages' default build preset (which runsnpx @11ty/eleventydirectly) picks it up without me needing a custom command. - UI: Pagefind's stock UI module styled to match the host theme. Loaded only on pages with a
#searchcontainer. - Performance: search box is interactive in well under 100 ms because the bundle is small and the index loads lazily.
Form architecture
I do not run a form server. Every form on every site I ship posts directly to Web3Forms, which routes the submission to the client's email.
- Submission flow: form POSTs
multipart/form-datatohttps://api.web3forms.com/submitwith the client's access key. Web3Forms validates, applies rate limits, and emails the client. - Spam protection: a hidden honeypot field (
name="botcheck") catches the dumb bots. Smart bots are caught by Web3Forms' rate-limit + heuristic. I do not use CAPTCHA; it kills conversion and is mostly defeated anyway. - Validation: HTML5 native (
required,type="email",pattern). Errors are accessible: invalid fields getaria-invalid="true"and a visible message announced viarole="alert". - Success behavior: on success the page redirects to
/thank-you/?type=<form-name>. The thank-you page reads the type parameter and shows form-specific next-steps. - Failure behavior: on network error the form shows an inline error (
#form-error) with phone and email fallback so the visitor still has a path to reach you. - What I never do: store form submissions on infrastructure I control. I do not see your messages unless you forward one to me. The data lives in your inbox.
- When Web3Forms isn't enough: for routing logic, conditional fields by zip code, or CRM integration I use a Cloudflare Worker (serverless edge function). Still no traditional server.
SEO architecture
SEO splits cleanly into two halves: technical foundation (binary; you have it or you don't) and ongoing content (gradual). I deliver the foundation; the content is yours.
- Per-page metadata. Title, description, canonical URL, Open Graph tags, Twitter Card tags. All are set in front-matter or layout defaults; never duplicated across pages.
- Structured data (JSON-LD):
WebSite+LocalBusiness+BreadcrumbListon every page.Serviceon each service page.BlogPostingon each post.FAQPageon FAQ.Personon author pages. All validated against schema.org and Google's Rich Results Test before launch. - Heading hierarchy. One
<h1>per page, descending order, no skipped levels. Page sections use<section>with a heading. - Sitemap. A custom
sitemap.njkemitssitemap.xmlby iteratingcollections.all. Submitted to Google Search Console at launch and refreshed on every deploy. - robots.txt. Allows everything by default; references the sitemap. No accidental disallows.
- Internal linking. Service area pages link to neighboring areas; service pages cross-link related services. Every page is reachable in two clicks from the homepage.
- URL hygiene. Trailing slash, lowercase, hyphenated, never abbreviated.
/services/water-heater-install/, not/svcs/whInstall. - Mobile-first. Google's index is mobile-first; my designs are too.
Accessibility pipeline
The working baseline is WCAG 2.2 Level AA. I test against it on every site before launch and every time I ship a significant change. Methods:
- Automated audits: Lighthouse (Chrome DevTools), axe DevTools, and WAVE. I aim for zero criticals and zero serious issues.
- Keyboard-only navigation from the first focusable element to the last, on every page. Every interactive control must be reachable and operable.
- Screen reader testing: VoiceOver on macOS and iOS, NVDA on Windows. I read every page top to bottom to catch landmark, heading, and label issues.
- Contrast verification using TPGi Colour Contrast Analyser against 4.5:1 (normal text) and 3:1 (large text / UI components). Every text/background combination in both light and dark modes.
- Zoom testing: up to 200% browser zoom without horizontal scrolling and without content clipping.
- Reduced-motion: every animation and transition is disabled when
prefers-reduced-motion: reduceis set. I test this with the OS-level setting enabled. - Forced-colors testing: Windows High Contrast mode pages render legibly; I use
forced-colors: activemedia queries where component styling needs to defer to system colors. - Touch targets: 44x44 CSS pixel minimum on every interactive element.
What that means in practice
- Semantic HTML: proper
<nav>,<main>,<article>,<header>,<footer>landmarks; heading order; lists that are lists; buttons that are buttons. - Skip-to-content link as the first focusable element.
- Visible focus rings (3 px gold ring at 3 px offset) on every interactive control.
- 44x44 CSS pixel minimum tap targets.
- Form labels bound to inputs via
for/id. Errors announced viarole="alert". - Radio/checkbox groups wrapped in
role="radiogroup"/role="group"witharia-labelledbypointing at the visible label. - Decorative SVGs get
aria-hidden="true"andfocusable="false". Meaningful images get realalt. - Color is never the only way information is conveyed.
aria-current="page"on the active nav link so screen readers announce position.
Security posture
The easiest-to-hack code is the code that doesn't exist. I keep my attack surface close to zero by staying static.
- No server-side runtime. No PHP, no Node runtime, no application server on the hot path. There's nothing to RCE, no database to SQL-inject, no session store to hijack.
- No plugins. WordPress plugin ecosystems are a decade-long source of CVEs. I have zero of them.
- No database. No credentials to leak, no backups to orchestrate, no PII retained on infrastructure I control. Web3Forms submissions flow straight to client email.
- HTTPS on day one. Automatic SSL via Cloudflare's managed certs. HSTS headers with
preloadandincludeSubDomains; max-age 12 months. - Content Security Policy & security headers configured via
_headersfor every site:Content-Security-Policytightly scoped to known origins.X-Frame-Options: DENY+frame-ancestors 'none'.X-Content-Type-Options: nosniff.Referrer-Policy: strict-origin-when-cross-origin.Permissions-Policydenies camera, microphone, geolocation, and other powerful APIs by default.
security.txtshipped on every site so researchers know how to responsibly disclose.- Dependabot on every repo for alerting on npm package vulnerabilities. Surface area is small (a handful of build-time dev dependencies, zero runtime deps) but I track them.
- Signed commits. Every commit is GPG-signed and verified by GitHub.
Caching & invalidation
Cloudflare's edge caches static assets aggressively. I make sure invalidation is atomic so a deploy never serves a half-stale page.
- HTML: served with
Cache-Control: public, max-age=0, must-revalidateso the edge revalidates on every request. Cloudflare returns a 304 if unchanged; the round-trip is sub-50 ms. - CSS & JS: served with
Cache-Control: public, max-age=31536000, immutable. URLs include a content-hash query string, so a new deploy automatically invalidates returning visitors' caches without serving stale assets. - Images: served with the same
immutablepolicy. Filenames are content-hashed; nothing collides. - Fonts: served by Bunny with a year-long cache.
- Search index: Pagefind chunks are content-hashed and immutably cached.
Build & deploy pipeline
- Commit. Changes get reviewed and committed to the
mainbranch on GitHub. I sign every commit. - CI trigger. Cloudflare Pages detects the push and spins up a clean build container.
- Build.
npx @11ty/eleventycompiles templates, processes images into AVIF/WebP/JPEG responsive variants, generates favicons, writes the RSS feed, writes the sitemap.xml and HTML sitemap. - Minification. In production, HTML/CSS/JS runs through
html-minifier-terserwith conservative settings (preserves semantic whitespace where it matters). - Pagefind indexing. Runs in an
eleventy.afterhook so the Cloudflare Pages preset works out of the box. Generates/pagefind/*with pre-indexed search chunks. - Cache busting.
eleventy-auto-cache-busterappends a content-hash query string to every stylesheet and script reference so upgrades ship immediately to returning visitors without stale assets. - Deploy. Cloudflare pushes the build to the global edge in under 60 seconds. A preview URL is generated for every non-
mainbranch so I can share in-progress work with the client. - Roll-back. Every prior deploy remains available; if anything is wrong I revert in one click.
Environments & branching
main: production. Auto-deploys to the live URL on every push.- Feature branches: any branch name. Auto-deploys to
<branch>.<project>.pages.dev. Used for client review of in-progress changes. - Pull requests: get their own preview URL plus a commented-back link. I share these with clients before merging.
- Local dev:
npm run devornpm start. Cross-platform commands (noELEVENTY_ENV=prefix; that's bash-only). Hot reload via 11ty's built-in server. - Environment variables:
SITE_URLdefaults to a sensible value but can be overridden per environment. Secrets (Web3Forms keys, analytics IDs) live in environment variables, never in the repo.
Measurement & monitoring
- Core Web Vitals via Cloudflare's server-side RUM. Real-user data, aggregated across the entire visitor population, no client-side cookies needed.
- Uptime monitoring via Cloudflare's status API plus an external ping monitor (UptimeRobot) that alerts me by SMS if the site is down.
- Google Search Console connected at launch for every client, so I can see actual search impressions, clicks, and Core Web Vitals reports from Google's own crawl data.
- Bing Webmaster Tools: underrated, still worth ~10 to 15% of service-business traffic depending on the industry.
- Lighthouse CI on major changes to catch regressions in performance or accessibility before they go live.
- Form-submission monitoring: Web3Forms reports submission counts; if a form goes silent for 10 days I email the client to verify it's still working.
- Domain expiration monitoring: I watch WHOIS for every client domain I manage, so renewal reminders don't depend on the registrar's email.
Browser support matrix
- Tier 1 (full support): the latest two versions of Chrome, Edge, Safari, Firefox on desktop and mobile.
- Tier 2 (graceful degradation): Chromium-based browsers from 2 years back. CSS custom properties, Grid, Flexbox, container queries (with fallback), and the AVIF/WebP image stack are all supported.
- Tier 3 (functional but ugly): very old browsers (IE-era) get unstyled, semantic HTML. The site is still readable; it just doesn't have the typography and spacing.
- No-JS: every page works fully without JavaScript. Search and the theme toggle degrade gracefully; everything else (nav, forms, content) works the same.
- Reduced-data & reduced-motion: respected via media queries.
Failure modes
Every system has failure modes. Honest disclosure of ours:
- Web3Forms outage would prevent form submissions until they recover. The page shows a fallback message with phone and email so visitors aren't stranded. Web3Forms' uptime in my experience: 99.95%+, with no incident lasting more than 30 minutes.
- Cloudflare regional outage would affect visitors in that region. I monitor Cloudflare's status feed and post a status note on Twitter/X if I see one. The site is unaffected for visitors in other regions.
- Bunny Fonts outage means the site renders in the system font fallback (which is the design's "B-tier" appearance, still readable, still on-brand). No layout shift.
- DNS misconfiguration during a registrar transfer is the single biggest risk window for any site move. I coordinate transfers carefully and run with a 5-minute TTL during the change so I can roll back fast.
- A client domain expiring would take the site offline within 24 to 48 hours. I monitor WHOIS expiration directly and remind clients at 60, 30, and 7 days.
- A typo committed to
mainwould deploy. I mitigate via PR previews, my own pre-deploy checks, and one-click rollback. The longest exposure window I have ever had to a typo on production: 90 seconds, and the typo was in a footer copyright line.
Technical Q&A
Why 11ty over Astro?
Astro is a great tool; if it had existed when I started I might have picked it. Today, for service-business marketing sites, 11ty has less abstraction overhead, a stabler plugin ecosystem for my use case, and equivalent output. I would evaluate Astro case-by-case for anything that genuinely benefits from islands; most of my builds don't.
Why no JavaScript framework?
My sites don't have application state. There's no shopping cart, no multi-step form with validation across pages, no dashboard. Everything a framework is good at, I do not need. Progressive enhancement with a few hundred lines of vanilla JS covers every interaction on my sites: theme toggle, mobile menu, FAQ accordion, form submission, site search, glossary tooltips. A framework would add 30 to 100 KB of runtime and force me to care about hydration; that's a large cost for no benefit.
How do you handle a blog with 200+ posts?
11ty handles it fine; I have done it. Build times scale roughly linearly with post count. A 200-post blog builds in under 10 seconds on Cloudflare's CI. Incremental builds (the --incremental flag) are under a second locally while authoring. Pagefind indexes the entire corpus in milliseconds because it operates on the static HTML, not the source templates.
What if I need a contact form that does something other than email?
Web3Forms covers roughly 95% of small-business form needs. For the other 5% (CRM integrations, automated routing by zip code, conditional logic between steps), I use Cloudflare Workers (serverless edge functions). Still no server to maintain. For anything genuinely app-shaped (booking calendars, live chat, client portals with accounts), I integrate purpose-built SaaS (Calendly, Tawk.to, etc.) rather than building from scratch.
Can I see the source code before I buy?
Yes. The site you're reading is representative of what I build. You can view source in your browser. I am happy to share the GitHub repo of this site on request.
What's in your build pipeline that a typical agency doesn't have?
Honestly, nothing exotic. My edge is discipline, not novelty. I commit more time to measurement (Lighthouse CI, real-user monitoring, manual accessibility testing) than to shiny tooling. The right tools for a marketing site have been stable for five years; I have just chosen to use them well.
Can I run my own hosting?
Yes. The output is plain static files. You can host the build on any CDN (Netlify, Vercel, S3+CloudFront, GitHub Pages, your own nginx). I deploy to Cloudflare Pages by default because the cost is zero and the edge is excellent; I do not lock you to it.
Can I migrate off your stack later?
Easier than most. The site is plain HTML/CSS/JS once built. Content lives in Markdown files. There's no proprietary database export step. A junior developer could pick up the source repo and continue maintaining it. I would help with the handoff at no charge; I would rather keep a friendly relationship than make you stay through friction.
Do you do A/B testing?
Yes, lightweight. I deploy two versions of a page on different URLs and split traffic via a Cloudflare Worker. I do not bolt on Optimizely or VWO unless the testing volume justifies the runtime weight (it almost never does for a small business).
How do you handle user-generated content?
I do not, by default. If a client needs comments, reviews, or user submissions on the public site, I wire in a purpose-built service (Disqus, GoRated, native Google Reviews via API) rather than running my own moderation. UGC is a different operational burden than static publishing; I treat it as a separate feature, not a baseline.
What's the worst thing about your approach?
Dynamic content from non-technical clients is a weak point. If a client's use case genuinely needs "edit in a browser" content management (say, a 30-person team that each needs to write blog posts), the "email me what to change" model doesn't scale. In that narrow case I either bolt on a headless CMS (Sanity, Decap) or recommend the client work with an agency that specializes in WordPress or a full-fledged CMS. Most small-business clients don't have this problem; most small-business "CMS needs" are better served by three emails a month to the person who built the site.
Can I see your build config?
Yes. eleventy.config.js in any of my repos is the entry point. I share it on request. There are no secrets in it; secrets live in environment variables.