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.

Build-time features work, request-time features do not Two columns. Build time, supported: Server Components, fetch during build, generateStaticParams, generateMetadata, static GET route handlers, client components. Request time, unsupported in export: cookies and headers, server-side searchParams, middleware, ISR and revalidate, server actions, the default image optimiser, dynamic route handlers. One question sorts every feature: when does it run? Build time — works Server Components fetch during build generateStaticParams generateMetadata static GET route handlers client components output written to files Request time — not in export cookies(), headers() server searchParams middleware ISR / revalidate server actions default image optimiser move to host config or edge functions
Every limitation below is a request-time feature; every workaround moves the work to build time or to the edge.

Limitation by Limitation

FeatureIn exportStatic equivalent
Middleware (middleware.ts)not supportedhost redirects/headers files; edge function for logic
redirects() / rewrites() / headers() in next.config.jsignored_redirects / _headers (Cloudflare, Netlify), vercel.json, CloudFront Functions
ISR (revalidate)not supportedscheduled rebuilds or build hooks on content change
Dynamic routes without generateStaticParamsbuild errorenumerate every param; dynamicParams = false
cookies(), headers(), draftMode()build errorclient-side reading; edge function for personalisation
searchParams in Server Componentsempty at buildread useSearchParams in a client component
Route handlers (non-GET or request-reading)not supportedserverless function on the host
Server Actionsnot supportedform posting to a function endpoint
next/image default loaderbuild errorcustom loader or build-time variants
Internationalised routing (i18n config)not supportedexplicit 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.

Time from CMS publish to live page Two timelines after an editor publishes. With on-demand ISR, the page is regenerated on the server and live in about 5 seconds. With a static export, a webhook triggers a build of about 80 seconds and a deploy of about 30, so the change is live in about 2 minutes, with a 60 second debounce window added for bursts of edits. Publish to live: ISR vs rebuild On-demand ISR ~5 s Export + hook debounce 60 s build 80 s deploy ~2–3 min For a content site, minutes of freshness lag was an easy trade for no server
The debounce keeps a burst of edits to one build; without it, ten saves in a minute meant ten deploys.

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.

Where each server feature goes after export Arrows map server features to static-host equivalents. next.config redirects and headers go to _redirects and _headers files. Middleware goes to an edge function. ISR goes to build hooks and scheduled rebuilds. Server actions and dynamic route handlers go to serverless functions. The image optimiser goes to a custom loader. Nothing is lost; it moves to the host next.config redirects / headers middleware ISR / revalidate server actions, POST handlers image optimiser _redirects, _headers (generated) edge function build hook + scheduled rebuild serverless function custom loader / build-time variants
The first row is the dangerous one: it is ignored silently rather than failing the build.

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:

MeasureServer runtimeStatic export
TTFB p75 (RUM)210 ms44 ms
Features needing a workaround5 (redirects, headers, ISR, contact form action, images)
Content freshness after CMS publish~5 s (on-demand ISR)~2 min (build hook)
Monthly hosting20 USD + usage0

Pitfalls & Rollback

  • Trusting next.config.js redirects. They are ignored in export; generate host files.
  • dynamicParams left true. Unlisted paths 404 in production while working in next dev.
  • Reading searchParams on the server. It is empty at build time; move the logic into a client component.
  • Forgetting the RSC payload files. Client navigation fetches .txt payloads; 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.