Lesson 55 of 81
output: 'export' turns next build into a folder of plain HTML/CSS/JS you can host anywhere — and the price is every feature in the App Router that assumes a server is still listening after that build finishes.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Setting output: 'export' in next.config.ts changes what next build produces: instead of a server-ready .next build, it emits a plain out/ folder — one HTML file per route, plus CSS and JS — deployable to any static host with zero Node.js runtime involved. Server Components still run, but only once, at build time, the same way traditional static-site generation always worked; there's no request-time server left to run them again per-visit.
That's also exactly the constraint: anything that genuinely needs a live request to do its job — reading a cookie, handling a dynamic route Next.js can't enumerate in advance, running proxy.ts, revalidating a cache on a timer — has nothing to run on anymore. Static export isn't a smaller version of the App Router; it's the App Router with the server half of the box checked off entirely.
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
};
export default nextConfig;
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPostSlugs();
return posts.map((slug) => ({ slug }));
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug); // runs once, at build time
return <article>{post.content}</article>;
}
// app/account/page.tsx — needs live, per-visitor data, so it stays client-rendered
"use client";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export default function AccountPage() {
const { data } = useSWR("/api/account", fetcher);
if (!data) return <p>Loading…</p>;
return <p>Welcome back, {data.name}</p>;
}
generateStaticParams is what makes the dynamic blog route exportable at all — without it, Next.js has no list of slugs to render at build time and nothing to serve for any of them afterward. AccountPage solves the opposite problem: genuinely per-visitor data that can't be known at build time gets fetched from the browser after the page loads, exactly like a traditional SPA — the static export still ships a real page, it just defers the personalized part to client-side JavaScript.
proxy.ts, cookies(), Server Actions, on-demand or time-based revalidation, Draft Mode, Intercepting Routes, and next.config.js rewrites/redirects/headers all require a live request-handling server that static export doesn't have.generateStaticParams(), and dynamicParams effectively has to stay off. Without a pre-generated list of params, there's no server left at request time to render an unanticipated value on the fly — a param Next.js didn't know about at build time has nowhere to be served from.GET only, and they render once into a static file during next build. A Route Handler that reads anything from the incoming request — query params, headers, the method — can't be part of a static export; it has no live request to read from.next/image's default loader is unsupported — a static export needs a custom loaderFile pointing at an external image service, since there's no server left to run Next's own on-demand image optimization endpoint.next build produces out/blog/post-1.html, not a server that maps /blog/post-1 to that file at request time — deploying to Nginx, GitHub Pages, or any other static host means configuring that host's own rewrite rules (try_files, or an equivalent) to serve the right .html file for a pretty URL.This sits at the far end of the deployment-shape decisions covered by the self-hosting-versus-Vercel and Docker deployment lessons — a single tradeoff between "keep a server running" and "ship files and walk away." It's also a useful contrast against the lesson on time-based revalidation: ISR's entire value proposition assumes a server is still there to regenerate a page on a schedule, which is precisely the thing static export gives up.