Skip to main content
PProfileMatch

Next.js Static Export, Deployment and Production Mistakes

Web DevelopmentLesson 4 of 46 min read

What you'll learn

  • Configure output: "export" and list the features that are unavailable in a static export
  • Choose between a Node server, a standalone Docker build and a static host
  • Handle environment variables correctly, including NEXT_PUBLIC_ values inlined at build time
  • Run a pre-launch checklist that catches the most common production failures

On this page

  1. Turning on static export
  2. What a static export cannot do
  3. Choosing where to deploy
  4. Environment variables: the build-time trap
  5. A pre-launch checklist
  6. Common mistakes
  7. How this shows up in real projects and interviews

A developer conference needs a microsite for three weeks: a schedule, speaker pages and a venue map. There is no login, no database and no budget for a server that must stay awake. Next.js can pre-render the whole site into plain HTML files and let any static host serve them. That is called a static export. It is a legitimate and cheap choice, but the moment you reach for a feature that needs a server, it silently stops being possible. This lesson is about knowing where that line is, and about the mistakes that cause trouble once a Next.js app is live.

Turning on static export

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: "export",
  trailingSlash: true,        // /schedule/ -> schedule/index.html
  images: { unoptimized: true },
  // basePath: "/devsummit",  // only if the site lives under a sub-path
};

export default nextConfig;

With output: "export", running next build writes a folder named out/ containing HTML, JavaScript, CSS and assets. You upload that folder to any static host, or preview it locally with a simple static file server. next start is not used for an export. Your components still run on the server, but only once, at build time, so Server Components are fine: they simply produce HTML that is saved to disk.

A dynamic route needs to say which pages exist, because there is no server to work it out later. generateStaticParams returns the list of parameter values to build.

app/speakers/[slug]/page.tsx
import { notFound } from "next/navigation";

type Speaker = { slug: string; name: string; talk: string };

const SPEAKERS: Speaker[] = [
  { slug: "ines-okafor", name: "Ines Okafor", talk: "Debugging production at 3 am" },
  { slug: "lukas-brandt", name: "Lukas Brandt", talk: "Small teams, big migrations" },
];

export function generateStaticParams() {
  return SPEAKERS.map((s) => ({ slug: s.slug }));
}

type Props = { params: Promise<{ slug: string }> };

export default async function SpeakerPage({ params }: Props) {
  const { slug } = await params;
  const speaker = SPEAKERS.find((s) => s.slug === slug);
  if (!speaker) notFound();

  return (
    <main>
      <h1>{speaker.name}</h1>
      <p>Talk: {speaker.talk}</p>
    </main>
  );
}

After the build, out/speakers/ines-okafor/index.html and out/speakers/lukas-brandt/index.html exist, each a finished page. A URL for a speaker who is not in the list has no file, so the host serves its 404 page. Adding a speaker means rebuilding and redeploying, which is the central trade-off of a static site: content changes need a build.

What a static export cannot do

The documentation lists the features that need a running server and are therefore unsupported in an export. The exact list can shift between releases, so treat this table as the general shape and confirm it for your version.

FeatureIn a static exportWhy
Dynamic routes without generateStaticParamsNot supportedNo server to render unknown values
cookies(), headers(), per-request searchParamsNot supportedThey need a real request
Server Actions and mutating route handlersNot supportedNothing is listening for POSTs
Redirects, rewrites and custom headers in the configNot appliedConfigure them on your host instead
Middleware (named proxy in the newest versions)Not supportedRuns per request on a server
Incremental static regeneration and revalidateNot supportedPages are never rebuilt after export
Default image optimisationNeeds unoptimized or a custom loaderThe optimiser is a server feature
Server Components that fetch at build timeSupportedThe data is baked into the HTML

Choosing where to deploy

TargetBuild outputUse it when
Managed Next.js platformDefault .next build, handled by the platformYou want every feature, including ISR and middleware, with little operations work
Your own Node servernext build then next startYou control a VM or container and need server features
Docker imageoutput: "standalone" copies only the needed filesYou deploy containers; see Docker
Static host or CDNoutput: "export" and the out/ folderContent sites with no per-request logic

With output: "standalone", next build produces a minimal folder under .next/standalone that includes a server.js, which keeps container images small. You still need to copy public and .next/static next to it, because they are not included automatically. For automated builds and releases, wire these commands into a pipeline as covered in CI/CD.

Environment variables: the build-time trap

.env.production (example values only)
# Server only: never reaches the browser
DATABASE_URL=postgres://app:change-me@db.internal:5432/app
REVALIDATE_TOKEN=generate-a-long-random-string

# Public: inlined into the JavaScript bundle at BUILD time
NEXT_PUBLIC_SITE_URL=https://devsummit.example
NEXT_PUBLIC_FORM_ENDPOINT=https://forms.example.test/submit/abc123

Variables whose names start with NEXT_PUBLIC_ are replaced with their literal values when the app is built, then shipped to every visitor. This has two consequences. First, never put a secret in one of them. Second, changing the value on your host after the build changes nothing in the client code; you must rebuild. A Docker image built once and promoted from staging to production will carry the staging values in its browser bundle. Variables without the prefix are read on the server at runtime, except where a page is prerendered at build time, which bakes in whatever they were during the build.

A pre-launch checklist

Run these before you call it production
[ ] npm run build succeeds locally AND in CI with a clean install
[ ] Tested with next build + next start (or the out/ folder), not only next dev
[ ] No secret appears in the browser bundle (search the built files for it)
[ ] NEXT_PUBLIC_* values are correct for THIS environment before building
[ ] metadata: title, description, metadataBase, Open Graph image on key pages
[ ] app/not-found.tsx and app/error.tsx exist and look intentional
[ ] robots.txt and sitemap are served; staging is blocked from indexing
[ ] Images: width/height set, remote hosts allowed in config, or unoptimized for export
[ ] Deep links work on the host (refresh /speakers/ines-okafor/ directly)
[ ] Logs and an error monitor are connected

Common mistakes

  • Adding `output: "export"` to an app that uses a server feature. The build stops with an error naming the unsupported feature, for example a dynamic route that lacks generateStaticParams, or a message that the default image loader is not compatible with export.
  • Image optimisation on a static host. Without images: { unoptimized: true } (or a custom loader) the export build fails; with an unconfigured remote image host you get hostname is not configured under images in your next.config.
  • Deep links returning 404 on the host. Visiting /schedule works via the in-app Link but fails on refresh because the host looks for schedule.html. Use trailingSlash: true so files land at schedule/index.html, or configure the host's routing.
  • Changing `NEXT_PUBLIC_` values after the build. The old value is still in the JavaScript. Rebuild with the new value.
  • Wrong `basePath` or asset prefix on project hosting. A site served under /devsummit/ without basePath shows unstyled pages and 404s for scripts, which you will see in the Network tab.
  • Shipping a build made with development settings or stale cache. Run a clean build in CI instead of deploying from a laptop.
  • No error or not-found pages. Users see the framework's generic screens, and failures disappear because nothing logs them.

How this shows up in real projects and interviews

At work, deployment decisions are rarely about which button to click and mostly about which features you need: if the answer includes auth, per-user pages or on-demand revalidation, static export is out. Interview questions here are practical: "how would you deploy a Next.js app with Docker?", "what is the difference between next build and next export?" (the standalone next export command was replaced by the output: "export" config option in recent versions), and "why are my environment variables undefined in the browser?" The latter is usually a missing NEXT_PUBLIC_ prefix. Pair this lesson with Node.js for server hosting basics.

Try it yourself

Take the conference microsite idea: a home page, a /schedule page and two speaker pages using generateStaticParams. Enable output: "export" with trailingSlash, run next build, and open the out/ folder to find the generated files. Serve it with any static file server, refresh a speaker URL directly, then deliberately add cookies() to one page and read the build error.

Show a hint

Look for out/speakers/<slug>/index.html. The cookies() error appears at build time, which is the point: export mode surfaces server-only features early.

Key takeaways

  • output: "export" pre-renders the site into an out/ folder of static files; it removes everything that needs a request-time server.
  • Dynamic routes in an export need generateStaticParams, images need unoptimized or a custom loader, and redirects and headers move to the host.
  • NEXT_PUBLIC_ variables are inlined at build time and visible to everyone; secrets must never use that prefix.
  • Test with a production build, check deep links on the real host, and keep a written pre-launch checklist.

Test yourself

A trick from this lesson's ride: Export means a folder of files: upload out/ and there is no server to run. Four more tricks like it wait along the route, each followed by a question.

Ride this lesson

Frequently asked questions

How do I export a Next.js app as static HTML?

Set output: "export" in your Next.js config and run next build. The static files are written to the out/ folder, which you can upload to any static host. Dynamic routes need generateStaticParams, and images need images: { unoptimized: true } or a custom loader.

What does Next.js static export not support?

Anything that needs a server at request time: cookies and headers, Server Actions, middleware, redirects and rewrites in the config, incremental regeneration and the default image optimiser. The exact list varies by version, so confirm it in the documentation for the one you use.

Why are my environment variables undefined in the browser in Next.js?

Only variables prefixed with NEXT_PUBLIC_ are exposed to browser code, and they are replaced at build time. A variable without the prefix stays on the server, and a public variable changed after the build needs a rebuild to take effect. Never put secrets in public variables.