ibit
Build log

Static export: where the limits are

WRWhite Rabbit·June 29, 2026·4 min

ibit ships as a folder. next build runs, and out drops a tree of HTML, CSS, and JS — no Node process behind it, nothing to keep warm, nothing to page you at 3am. The whole thing is one line of config:

// next.config.ts
const nextConfig: NextConfig = {
  output: "export",
  images: { unoptimized: true },
};

That one line is a contract, and the interesting part of the contract is what it takes away. The mental model I keep coming back to: anything that needs to know something at request time is gone, because there is no request time. There's only build time and the browser. If a feature lives in the gap between those two — on a server that answers each hit — it's off the table.

What still works (more than you'd guess)

Static export isn't "the SPA tier." Server Components run — they just run during next build instead of per request, like classic static-site generation. A fetch in a server component fires once at build, bakes its result into the HTML, and ships. So you keep the React Server Component model and the data-at-build pattern; you only lose the per-request part.

You also keep client-side everything: 'use client', hooks, useSWR fetching live data from the browser, client routing between prebuilt pages. It behaves like an SPA once loaded, but each route is a real prerendered HTML file underneath. Even Route Handlers survive in a narrow form — GET only, evaluated at build, written out as a static file:

// app/data.json/route.ts → emitted as /data.json at build
export async function GET() {
  return Response.json({ name: "Lee" });
}

The catch is in that sentence: it runs at build. The moment a handler reaches for the incoming Request, you're asking for a server, and you can't have one.

The wall

Here's the boundary, straight from the static-exports guide for the version this project pins. These error out rather than degrade:

How the wall actually feels

The honest version: you rarely hit these limits head-on. You hit them sideways, six months in, when you reach for a redirect or a quick auth cookie out of habit and the build refuses. The fix is almost always to push the work to one of the two real places — bake it at build, or do it in the client — or to move that one piece to an edge function and stop pretending the whole site is dynamic.

For a blog, the trade is barely a trade. Redirects move to the host's config (Vercel, Netlify, an Nginx try_files block — the guide even ships an example). Search runs client-side over a prebuilt index. There's no server to patch, no cold start, no attack surface that isn't just static files. The constraint that looked like a cage is mostly a forcing function: it keeps you from accidentally depending on a server you didn't want to run.

Static export isn't Next.js with features removed. It's Next.js with the server removed — and once that reframing lands, the list of "unsupported" features stops reading like a list of bugs and starts reading like a definition.