Build Tooling
Static Site, Zero Backend (L0)
Reading level
The server you didn't need
A developer builds a personal portfolio with React and Express. They deploy to a VPS: $20/month, manual SSL renewal, security patches, uptime monitoring. The site has 5 pages and never changes. It has no user accounts, no database, no dynamic content. It just needs to show text and images.
Three months later they migrate to 11ty on Netlify. The same 5 pages, now as static HTML files on a CDN. Cost: $0. Load time: 3× faster. Downtime: zero. Server maintenance: zero. They got a better product with less engineering.
The question to ask before reaching for a server: does this content depend on who is asking? If not — if the same HTML would be served to every visitor — it's static content. Serve it from a CDN, not a server. A CDN edge node 20ms from the user will always beat an origin server 200ms away, with no code execution, no cold starts, and no runtime failures.
The architectural principle: "L0" — serve pre-rendered HTML from globally distributed CDN PoPs. No server, no database, no runtime. The only moving parts are the build (triggered by git push) and the CDN cache invalidation (triggered by the build). Operational complexity is zero. TTFB is CDN-to-user latency, typically 5–40ms. This architecture is correct for any content site until proven otherwise.
Static site generators and the deployment model
How it works: you write Markdown/Nunjucks/MDX, run a build command, and get a folder of HTML/CSS/JS files. You deploy that folder to Netlify, Vercel, or GitHub Pages. CDN serves the files globally with no server needed.
When you need dynamic features: almost always solvable without a full backend:
- Contact form → Netlify Forms, Formspree (no backend needed)
- Comments → Utterances (GitHub issues), Giscus
- Newsletter signup → Buttondown, Mailchimp embed
- Analytics → Plausible, Fathom (script embed)
- Search → Pagefind (static search index), Algolia
The decision matrix:
- Same content for all users → static (11ty, Astro, Hugo, Jekyll)
- Some pages need user data, most don't → hybrid (Next.js ISR, Astro islands)
- Almost every page needs auth or real-time state → full server (Next.js SSR, Remix, Express)
# 11ty build: runs at deploy time, not per-request
npx @11ty/eleventy --input=src --output=dist
# Output: static HTML files
dist/
index.html
about/index.html
blog/post-1/index.html
assets/style.css
# Deploy: just upload the dist folder
netlify deploy --dir=dist --prod
The operational cost argument:
- No cold starts — CDN serves pre-built files; no function initialization latency
- No scaling events — CDN absorbs traffic spikes (HN front page, Product Hunt) without configuration
- No patching — no OS, no runtime (Node, Python), no web server (nginx, Apache) to maintain
- Rollback — is a CDN cache invalidation, triggered by redeploying a previous build artifact
- CI/CD — git push → build → deploy is the entire pipeline; no Kubernetes, no ECS, no load balancer
When you genuinely need dynamism: add a serverless function (Vercel API routes, Netlify Functions, Cloudflare Workers). One function for a contact form or an auth callback doesn't justify a full server.
The server that went down for content that never changed
A developer's marketing site for a design agency ran on an Express server on a $20/month VPS. Five pages: Home, About, Work, Services, Contact. No user accounts. No database. The content changed maybe once a month when they won a new client.
One Tuesday, the site went down. Not because of traffic — a misconfigured nginx reverse proxy update broke the routing. Ten minutes of downtime during business hours, a panicked phone call, a manual SSH fix. The content itself was fine. The five HTML pages were exactly what they'd always been. The server had failed to serve files that hadn't changed in three weeks.
The technical cause was architectural: every page request initiated a server-side render cycle — Express received the request, loaded a Pug template, rendered HTML, and sent a response. None of this work was necessary. The output was identical for every visitor, every request, every day. The server added a 180ms round-trip, an nginx process that could misconfigure, a Node.js runtime that could crash, and an SSL certificate that needed renewal — for the privilege of serving five static files.
The nginx misconfiguration was the trigger, but the real bug was choosing a server-rendered architecture for content with zero dynamic requirements.
Systemic impact: every server-rendered content site is a liability surface that doesn't need to exist. The operational cost — patching OS, maintaining Node.js runtime versions, monitoring uptime, managing SSL renewal, handling configuration drift — is fixed overhead with no corresponding value when the content is static. In an agency context with dozens of client sites, this overhead multiplies per site. One misconfiguration event per year per site, at 10 minutes of downtime each, across 20 client sites, is 3+ hours of avoidable incidents annually. The architecture itself is the bug.
Five files on a CDN, zero servers to break
The team rebuilt the site in Eleventy over a weekend. Same design, same content, same five pages — now as plain HTML files with the CSS inlined. They deployed to Netlify. Cost: $0. The deploy was a git push: Netlify ran npx @11ty/eleventy and published the output folder to its CDN.
There was no server to go down. The nginx that had caused the outage no longer existed. SSL was managed by Netlify automatically. The contact form went to Netlify Forms — no backend needed. Rollback was clicking "Publish deploy" on a previous build in the Netlify dashboard.
The implementation change: Eleventy's build runs once at deploy time, not per-request. Templates compile to HTML files. Those files live on Netlify's CDN edge nodes globally. A request for /about/ hits an edge PoP 15ms from the user, returns cached HTML with no compute. TTFB dropped from ~180ms (origin round-trip) to ~18ms (CDN edge). The "Cache-Control: public, max-age=31536000, immutable" header meant the file was cached indefinitely — until the next deploy invalidated it. No server to patch, no runtime to update, no uptime to monitor.
Measurement: compare deploy pipelines before and after. Before: SSH to VPS → git pull → pm2 restart → nginx reload → manual smoke test → 8 steps, 12 minutes, human required. After: git push → Netlify webhook → build (45 seconds) → atomic CDN deploy → automatic deploy preview URL → 4 automated steps, zero humans required for standard deploys. Rollback SLA: before, 20+ minutes (requires SSH, investigation, manual fix). After, 30 seconds (one click in the Netlify dashboard to promote any previous build). The architecture change made the deployment pipeline measurably safer, faster, and autonomous.
Pattern at a glance
Annotated example: request path for a marketing page
❌ SERVER-RENDERED
Browser
→ nginx (can misconfigure)
→ Node/Express (can crash)
→ template render
→ HTML response
~180ms TTFB, server required
Every request pays runtime cost; server outage = site outage; content identical for every user
✅ STATIC CDN
git push → Eleventy build
→ HTML files → CDN PoPs
Browser
→ CDN edge (15ms away)
→ cached HTML
~18ms TTFB, no server
Pre-built at deploy time; no runtime to fail; 10× faster TTFB; rollback in 30 seconds
Try it: server-rendered vs static delivery
"With Backend" shows a blog page making a server round-trip to fetch posts on every visit. "Static" shows the same page pre-built — the HTML is already there, no fetch needed. Open the Network tab and compare the timing.
The TTFB difference is the server round-trip vs CDN edge latency. For content that never changes per-request, you're paying a 150–300ms server penalty on every page view, for every user, forever — when the CDN alternative is 5–30ms.
Check the Response Headers in each mode. The static version returns from a CDN PoP (X-Cache: HIT or CF-Cache-Status: HIT). The server version returns from the origin on every request (no cache header or Cache-Control: no-store). The architecture difference is visible in the headers.
Showing: Static — CDN delivery
Choosing and building with static site generators
The three most common SSGs for frontend developers right now: Eleventy (11ty) — zero-config, works with any template language, outputs plain HTML; best for blogs, portfolios, docs. Astro — component islands model, supports React/Vue/Svelte components that are zero-JS by default; best for content sites that need some interactivity. Hugo — fastest build times (1,000+ pages per second), Go templates; best for large content libraries.
# 11ty: simplest start
npm init; npm i @11ty/eleventy
echo "# Hello" > src/index.md
npx @11ty/eleventy --serve
# Astro: for component islands
npm create astro@latest
# Astro ships zero JS by default
When static isn't enough — the honest decision matrix:
- Auth / personalization: static can't render per-user content at the CDN layer. Use Astro SSR for specific routes, or Next.js with route-level rendering decisions.
- Real-time data: static + client-side fetch on load (e.g., stock prices, live scores). The shell is static; the data is dynamic via API call in the browser.
- Frequent content updates (>10/day): Incremental Static Regeneration (ISR) in Next.js or Astro's on-demand revalidation — pages rebuild automatically when content changes, without a full site rebuild.
// Next.js ISR: revalidate every 60 seconds
export async function generateStaticParams() { ... }
export const revalidate = 60; // seconds
// Astro on-demand: rebuild this page when called
export const prerender = false; // specific route only
The hybrid middle ground — Incremental Static Regeneration (ISR) and on-demand revalidation — lets you treat static delivery as the default while adding server capability only where proven necessary. The production pattern: deploy to Vercel/Netlify with edge functions for the dynamic subset of routes. Measure the split: if <5% of routes need server rendering, ISR covers the remaining 95% at CDN speed. The key architectural decision is route-level, not site-level — most teams overbuild to full SSR when only their dashboard and user-profile routes need it. Profile before deciding: add x-vercel-cache or CF-Cache-Status logging to understand your actual cache hit rate by route. Routes with <1% cache hits are SSR candidates; routes with >90% hit rate are ISR/static candidates. Operate from data, not assumption.
References
Remember
Key takeaways
-
If every visitor sees the same content, you don't need a server. Use a static site generator (11ty, Astro, Hugo) and deploy to a CDN — faster, cheaper, zero maintenance.Ask before every architecture decision: "does this content depend on who is asking?" If no, serve pre-built HTML from a CDN. If yes, only then consider a server or serverless function.L0 static architecture has zero cold starts, zero scaling events, zero runtime patches, and CDN-edge TTFB of 5–40ms. It's the correct default for content sites until dynamic requirements are proven.
-
Contact forms, comments, search, analytics — almost every "dynamic" feature a portfolio or blog needs has a static-compatible solution without a full backend.For sites with some dynamic pages: hybrid architectures (Next.js ISR, Astro SSR for specific routes) keep the static-delivery benefits for most pages while adding server capability only where needed.Rollback for static sites is a CDN cache invalidation triggered by redeploying a previous artifact — the safest deployment model that exists. No migration scripts, no state rollback, no downtime risk.
Keep going
Finish this takeaway, then continue the track — Casey saved your spot locally.
Sign in with email to sync progress across devices (beta).
Inside the Casebook
New cases every few weeks — patterns from production UI engineering. Double opt-in, easy unsubscribe.
No spam. Unsubscribe anytime. Emails sent via Buttondown.
RSS feed