Back to the blog

Next.js Static Export (output: 'export'): The Complete Guide

Next.js Static Export (output: 'export'): The Complete Guide

10 October 2026 — Next.js

Published

10 October 2026

Reading time

4 min read

Category

Next.js

Tags

  • Next.js
  • Static Export
  • App Router
  • Hosting

(Intro)How Next.js static export works in the App Router, what it can't do, and the workarounds for images, dynamic routes, forms and redirects.

A statically exported Next.js site is just HTML, CSS, JavaScript and assets in a folder. You can put that folder on any static host — Cloudflare Pages, GitHub Pages, S3, Netlify, an Nginx box — with no Node.js server to run, patch or pay for. This site is built exactly that way.

This guide covers how static export works in the App Router, what you give up, and how to work around each limitation.

Turning it on

Since Next.js 14, next export is gone; static export is a config option:

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export',
  // Optional: /about → /about/index.html instead of /about.html
  trailingSlash: true,
  images: { unoptimized: true },
}

export default nextConfig

next build now writes the site to out/. Preview it with any static server:

bash
npx serve out

What works

More than people expect:

  • Server Components. They run once, at build time. Fetch data, read files, call APIs — the result is baked into HTML.
  • Client Components, hooks, context and every client-side library.
  • Dynamic routes, as long as you list every path with generateStaticParams.
  • Route Handlers with GET, which become static files (handy for feed.json, sitemap.xml, robots.txt).
  • The Metadata API: titles, descriptions, Open Graph, canonical URLs.
  • Client-side navigation with <Link> and prefetching.

What doesn't work, and what to do instead

Feature Why it fails Workaround
Default image optimization Needs a server images.unoptimized: true, pre-sized images, or a custom loader (Cloudinary, imgix, Cloudflare Images)
Dynamic routes without generateStaticParams Pages must be known at build time List all params; set dynamicParams = false
cookies(), headers(), request-time data No request at runtime Read them on the client, or move logic to an API
Server Actions Need a server POST to a serverless function (Workers, Lambda)
Middleware Needs a server Host-level rules: _redirects, _headers, edge functions
redirects, rewrites, headers in config Applied by the Next server Same: configure them at the host
ISR / revalidate No server to regenerate Rebuild on content change (a Git push or a webhook)
Built-in i18n routing Pages Router feature Locale segments: app/[lang]/... with generateStaticParams

Dynamic routes, done right

Every dynamic segment needs a list of its values at build time:

app/blog/[slug]/page.tsx
import { getAllPosts, getPost } from '@/lib/blog'
import { notFound } from 'next/navigation'

export const dynamicParams = false // unknown slugs are a 404, not a runtime render

export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map((post) => ({ slug: post.slug }))
}

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) notFound()
  return <article dangerouslySetInnerHTML={{ __html: post.html }} />
}

Note that in Next.js 15 params is a Promise — await it.

One gotcha: if generateStaticParams returns an empty array, the build fails, because the route would produce no pages. If a collection can be empty (say, a new blog), return a placeholder param and let it render a 404.

Images

You have three reasonable options:

  1. unoptimized: true and export images at the right size yourself (WebP or AVIF, 1–2× the displayed width). Simple and fast if you're disciplined.
  2. A custom loader pointing at an image CDN, which returns resized variants on demand:
image-loader.js
export default function loader({ src, width, quality }) {
  return `https://images.example.com/${src}?w=${width}&q=${quality || 75}`
}
next.config.mjs
const nextConfig = {
  output: 'export',
  images: { loader: 'custom', loaderFile: './image-loader.js' },
}
  1. Plain <img> with srcset, loading="lazy" and explicit width/height to avoid layout shift.

Forms and anything server-side

A static site can still have forms, search and logins — the server part just lives elsewhere. The usual pattern is a small serverless function that the page calls with fetch. I wrote up the one this site uses in a serverless contact form with Cloudflare Workers and D1.

Redirects and headers

Configure them at the host. On Cloudflare Pages and Netlify, a _redirects file in public/ is copied into out/:

public/_redirects
/old-page    /new-page    301
/blog/*      /journal/:splat    301

And _headers for caching and security headers:

public/_headers
/_next/static/*
  Cache-Control: public, max-age=31536000, immutable

Updating content without a server

Because there's no ISR, content changes need a rebuild. That sounds worse than it is: connect the host to your Git repository and every push builds and deploys in a minute or two. For content that lives in a CMS, point the CMS's webhook at the host's deploy hook. For a blog, keeping posts as Markdown in the repository is the simplest option of all — see a Markdown blog in Next.js without a CMS.

When static export is the wrong choice

  • Pages that depend on who is logged in, rendered on the server.
  • Thousands of pages that change every few minutes (build times grow with page count).
  • Anything that genuinely needs request-time rendering, like personalised pricing.

For marketing sites, portfolios, documentation, blogs and landing pages, static export is usually the fastest, cheapest and most robust option there is.

Checklist

  • output: 'export' in next.config
  • Every dynamic route has generateStaticParams and dynamicParams = false
  • Images handled (unoptimized, custom loader or <img>)
  • No cookies(), headers(), Server Actions or middleware
  • Redirects and headers configured at the host
  • Deploys triggered by Git pushes or CMS webhooks
  • out/ previewed locally before the first deploy

Next step: make sure search engines understand the pages you export — technical SEO for the Next.js App Router.

Newsletter

Get new posts by email

A short email when something new is up. No spam, and you can unsubscribe at any time.

Technical SEO for the Next.js App Router: A Practical Checklist
Technical SEO for the Next.js App Router: A Practical Checklist
Next post →