Next.js App Router Static Export Limitations
output: 'export' turns a Next.js App Router project into a folder of HTML, CSS, JavaScript and RSC payload files that any static host can serve. For content sites that is often exactly right: React Server Components still run, at build time, and the result deploys to a CDN with no server. But the App Router was designed around a server, and a significant set of its features either do not work in an export or behave differently. Teams usually discover them one build error at a time.
This guide lists each limitation in one place, explains why it exists, and gives the static equivalent — from the host's configuration, an edge function, or a build-time pattern. It is part of Next.js Static Export for Content Sites, complementing Handling Dynamic Routes in Next.js Static Export.
Prerequisites
- A Next.js 14 or 15 App Router project with
output: 'export'. - A static host with redirect, header and function support (Cloudflare Pages/Workers, Netlify, Vercel, or S3 with CloudFront).
- The build log from a first export attempt — the errors list most limitations for you.
The Rule Behind Every Limitation
Everything that runs at build time works. Everything that must run per request does not, because a static export has no request-time runtime. Server Components, fetch in Server Components, generateStaticParams, generateMetadata and static route handlers all run during next build and write their output to files. Cookies, headers, search params on the server, middleware, revalidation and on-demand image optimisation all need a request, and so fail or are disabled.
Limitation by Limitation
| Feature | In export | Static equivalent |
|---|---|---|
Middleware (middleware.ts) | not supported | host redirects/headers files; edge function for logic |
redirects() / rewrites() / headers() in next.config.js | ignored | _redirects / _headers (Cloudflare, Netlify), vercel.json, CloudFront Functions |
ISR (revalidate) | not supported | scheduled rebuilds or build hooks on content change |
Dynamic routes without generateStaticParams | build error | enumerate every param; dynamicParams = false |
cookies(), headers(), draftMode() | build error | client-side reading; edge function for personalisation |
searchParams in Server Components | empty at build | read useSearchParams in a client component |
| Route handlers (non-GET or request-reading) | not supported | serverless function on the host |
| Server Actions | not supported | form posting to a function endpoint |
next/image default loader | build error | custom loader or build-time variants |
Internationalised routing (i18n config) | not supported | explicit locale segments /[lang]/… with static params |
Redirects and Headers
The redirects() and headers() functions in next.config.js are applied by the Next.js server, which does not exist in an export. They are silently ignored — the most dangerous limitation because nothing fails. Generate the host's native file at build time from the same source so they stay in sync:
// scripts/emit-redirects.mjs — run after `next build`
import cfg from '../redirects.config.mjs';
import { writeFileSync } from 'node:fs';
writeFileSync('out/_redirects', cfg.map((r) => `${r.source} ${r.destination} ${r.permanent ? 301 : 302}`).join('\n') + '\n');
Redirect semantics per host are covered in Configuring Redirects on Cloudflare Pages and Netlify Redirects and Rewrites for Static Sites.
ISR and Freshness
Incremental Static Regeneration regenerates pages on a server after a time or on demand. Its static equivalent is a rebuild: triggered on a schedule for time-based freshness, or by a CMS webhook for on-demand updates. For a 1,200-page content site with an 80-second build, a webhook-triggered rebuild published a CMS change in about two minutes — slower than ISR's seconds, but without a server. The comparison is in Vercel ISR vs Static Generation for SSGs.
For most content teams two minutes is invisible; editors publish and move on. Where it is not — a status page, a live event schedule — a hybrid works: keep the page static, and let a small client component fetch the time-sensitive fragment from an edge function on load, as described in Proxying Third-Party APIs from an Edge Function.
Middleware Logic
Authentication gates, geolocation redirects and A/B routing that lived in middleware move to an edge function on the host, which runs before the static file is served. The logic ports nearly unchanged because both use Request and Response; see Protecting a Static Site Behind Authentication.
Trailing Slashes and Client Navigation
Two quieter behaviours affect how the exported files are served. By default, Next.js writes about.html and links to /about; with trailingSlash: true it writes about/index.html and links to /about/. Static hosts differ in how they map extensionless URLs to files, so choose the option that matches the host and assert it in smoke tests — the same issue that arises on S3, described in Clean URLs and Trailing Slashes on S3. Client-side navigation with <Link> fetches each route's RSC payload (index.txt or about.txt) rather than the HTML; the host must serve those files, with a sensible cache lifetime, or navigation silently falls back to full page loads. After a deploy, readers with an open tab request payloads from the previous build, so keep old files available briefly, which atomic hosts do by default.
Catching Silent Failures
Most limitations fail the build loudly. The silent ones — ignored redirects() and headers(), and searchParams being empty at build time — need tests. After each export, a smoke test against the preview deploy should request a sample of redirect sources and assert the 301s, fetch a page and assert the security headers, and load a page that depends on query parameters in a headless browser to confirm the client component reads them. The pattern is the same as in Running Smoke Tests Against a Preview URL. On the site that motivated this guide, 38 redirects defined in next.config.js had been silently ignored for three months after switching to export; the smoke test would have caught them on the first preview.
When Export Is the Wrong Choice
If the list of workarounds is long — per-request personalisation on most pages, minute-level freshness for thousands of pages, heavy use of Server Actions — a static export fights the framework. At that point either run Next.js on a server or edge runtime, or choose a generator designed for static output, as compared in Next.js Static Export vs Astro for Marketing Sites. For a content site whose dynamic needs are a form, a few redirects and daily freshness, export is a good fit.
Measured Impact
A 1,200-page App Router content site moved from Vercel's server runtime to output: 'export' on Cloudflare Pages:
| Measure | Server runtime | Static export |
|---|---|---|
| TTFB p75 (RUM) | 210 ms | 44 ms |
| Features needing a workaround | — | 5 (redirects, headers, ISR, contact form action, images) |
| Content freshness after CMS publish | ~5 s (on-demand ISR) | ~2 min (build hook) |
| Monthly hosting | 20 USD + usage | 0 |
Pitfalls & Rollback
- Trusting
next.config.jsredirects. They are ignored in export; generate host files. dynamicParamsleft true. Unlisted paths 404 in production while working innext dev.- Reading
searchParamson the server. It is empty at build time; move the logic into a client component. - Forgetting the RSC payload files. Client navigation fetches
.txtpayloads; the host must serve them with the right content type. - Rollback: removing
output: 'export'restores the server runtime; the host must then run Next.js again.
Conclusion
A Next.js App Router static export keeps everything that runs at build time and drops everything that runs per request. Middleware, next.config.js redirects and headers, ISR, server actions and on-demand image optimisation each have a static-host equivalent — generated config files, edge and serverless functions, build hooks and custom loaders. The silent ones need tests. For content sites whose dynamic needs are small, the trade is worth it: on a 1,200-page site, TTFB fell from 210 to 44 ms and hosting cost went to zero.
FAQ
Do React Server Components work in a static export?
Yes. Server Components run at build time and their output is written into the static HTML and RSC payload files. What does not work is anything that needs to run on each request, such as reading cookies or headers.
Can I use route handlers with output export?
Only GET route handlers that return static content, which are rendered to files at build time — useful for feeds, sitemaps and JSON. Handlers that read the request or handle POST need a serverless function on the host instead.
What happens to middleware in a static export?
It is not supported. Redirects, rewrites, headers and auth checks that middleware would perform must move to the host's configuration or edge functions, such as a _redirects file or a Cloudflare Worker.
How do I handle dynamic routes?
Every dynamic segment must be enumerated at build time with generateStaticParams, and dynamicParams must be false. Paths not returned at build time return 404, because there is no server to render them on demand.
Related
- Parent: Next.js Static Export for Content Sites — when export is the right model.
- Handling Dynamic Routes in Next.js Static Export — generateStaticParams in depth.
- Optimizing Images in Next.js Static Export — the image optimiser replacement.
- Serverless Functions for Static Sites — where request-time logic goes.
- Netlify Build Hooks for Content Updates — replacing ISR with rebuilds.