Every static-export project starts with the same conversation. A client, or occasionally our own engineering lead, asks why we're building on a stack that can't run a database query at request time, can't personalise a hero image by geography, can't do anything the moment a request lands beyond serving a file that was already built. The honest answer is that we're trading one kind of power for another. Static export gives up runtime flexibility in exchange for a smaller, faster, more honest surface area — and for the class of sites we build, motion-heavy, identity-first, marketing-forward, that trade is almost always correct. This site runs on exactly that architecture. Understanding why means being specific about what the tax actually buys, not just what it costs.
The tax nobody mentions
Static export means every route has to be knowable at build time. No middleware inspecting a request. No route handlers doing work when a visitor arrives. No incremental regeneration quietly refreshing a page in the background. No server components fetching anything fresh, because there is no server left once the build finishes — only files, sitting on a CDN, waiting to be handed out unchanged until the next deploy. next/image's optimisation loader doesn't run either; images ship as-is, sized and compressed ahead of time.
That's a real constraint, and it's unforgiving in a specific way: every dynamic feature stops being free and starts being a decision. A content edit means a rebuild, not a CMS save. A personalised greeting means client-side JavaScript reading something after the page has already loaded, not a server deciding what to send. You cannot accidentally end up with a dynamic feature on a statically exported site. You have to choose it, and justify it, every time.
What you get back
In exchange, the site becomes almost embarrassingly simple to reason about. One build produces a folder of files. That folder gets served from the edge, cached everywhere, with no cold start because there was never a function waiting to spin up. Ten visitors or ten million, the server-side cost is identical, because there effectively isn't one.
The security argument is the one that gets underweighted. Most server-side vulnerability classes — injection, broken auth checks, SSRF, a dependency with a request-time exploit — need server-side code executing per request to exist at all. Remove that code and you don't have to patch it, monitor it, or lie awake wondering if this week's CVE applies to you. A static file cannot be tricked into running someone else's SQL. The attack surface isn't reduced. Most of it is simply absent.
Cost follows the same logic. You're paying for bandwidth against static assets, not for compute that has to be provisioned, scaled and watched. It's the closest thing in web infrastructure to a fixed, predictable bill.
Where the constraint actually bites
The honest failure mode is anything that genuinely needs to happen per request: a form that has to persist somewhere, a price that changes by market, a dashboard behind a login. This site has exactly one such need — a lead-capture form — and the fix wasn't to abandon static export, it was to add a single Cloudflare Pages Function that does nothing except accept that one submission and write it to a database. The rest of the site never touches a server, and never needs to.
A static site with one carefully justified server function is not a compromise. It's a static site that knows exactly where its one exception lives.
That's the pattern worth naming: a Function is not a crack in the architecture, it's a deliberate, narrow escape hatch, invoked only for the route that needs it, doing one job, auditable in isolation. It's the opposite of quietly letting a 'just this once' server route sprawl into a parallel backend nobody planned for.
The discipline of saying no
The failure mode we actually watch for isn't the constraint — it's the temptation to route around it by default. A CMS 'just in case' content changes often. Server-side rendering 'just in case' a page needs fresher data someday. Both are reasonable requests in isolation and both quietly reintroduce the entire operational surface static export was chosen to avoid, for a need that, in most of our projects, never materialises.
The discipline is treating every request for dynamism as a question with three possible answers, in order of preference: can this resolve at build time, can this resolve with a client-side fetch after the page has already loaded, or does this genuinely need a single, scoped server function. Almost everything resolves to the first two. The third is rare enough that when it happens, it's worth writing down why.
When we reach for something else
None of this is an argument that static export is always correct. Real auth-gated dashboards, e-commerce with live inventory, anything with meaningful per-user state at scale — that's not what this architecture is for, and forcing it produces worse engineering than choosing a server-rendered framework, or a proper backend, from the first commit.
Part of the job is recognising which class a project belongs to before the first line of code, not after the constraint has already bitten. Most of the sites we build are identity, not infrastructure — a brand, a story, a handful of forms. For that class of work, the tax is small and the discipline it imposes, that every exception has to earn its place, tends to produce a cleaner system than starting from 'anything is possible' ever does.
Frequently asked questions
What does "static export" mean in Next.js?
Static export (output: "export" in next.config) builds every route to a plain HTML file at build time, with no Node.js server, middleware, route handlers, or ISR available at runtime. The output is a folder of static files you can host on any CDN or static host. Anything dynamic has to happen client-side or via a separate, explicitly added service.
Why not just use ISR or server components instead of pure static export?
ISR and server components need a server process running at request time, which reintroduces cold starts, server-side attack surface, and ongoing infrastructure to patch and monitor. For sites that don't need per-request personalisation, that's cost with no benefit. We reach for server rendering only when a project's data genuinely changes per request, not by default.
How do you handle things like forms or dynamic content on a static site?
Client-side JavaScript handles anything that only needs to happen in the browser after the page loads. For the rare case that needs true server-side persistence, like a lead form writing to a database, we add one narrowly scoped serverless function for that route alone, rather than reintroducing a general-purpose backend.