How to Fix: next config output=”export” with appDir doesn’t work
Next.js appDir and static export fail together in this issue because the project is mixing an App Router build with an export flow that was designed differently across Next.js versions. When output="export" is enabled, Next must be able to pre-render every route as static files. In the affected setup, the build still expects App Router behavior that cannot be completed through the old next export command.
Understanding the Root Cause
The bug happens because App Router support and static exporting changed significantly in Next 13. In older mental models, exporting a site meant running:
next build && next export
But with newer Next.js behavior, when you configure:
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
the framework already treats the application as a static export target during build time. That means next export is no longer the right command for this setup. For App Router projects, this mismatch often causes build or export failures because the project is being processed twice through incompatible expectations.
There is a second technical constraint: the App Router only works with static export if every page and every dependency used by those pages can be resolved into static output. Features that require a live server are incompatible, including:
- Dynamic server rendering
- Request-time data access
- Server-only runtime behavior
- Routes without static params where params are required at build time
- API routes intended to run on a Node server
So the root cause is usually one or both of these problems:
- Using next export together with output="export".
- Using appDir/App Router features that are not fully statically renderable.
Step-by-Step Solution
The safest fix is to treat the project as a true static site and let next build generate the export output by itself.
1. Update next.config.js
Use a minimal static export configuration:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
}
module.exports = nextConfig
If the project is deployed under a subpath or needs trailing slash behavior for static hosting, extend it carefully:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true,
},
}
module.exports = nextConfig
Why this matters: static hosts cannot run the default Next Image Optimization server, so images.unoptimized is commonly required.
2. Remove the next export command from the build flow
Change scripts in package.json from an old export flow to a build-only flow.
Before:
{
"scripts": {
"build": "next build",
"export": "next export"
}
}
After:
{
"scripts": {
"build": "next build"
}
}
If your CI previously ran:
yarn build && yarn next export
replace it with:
yarn build
The generated static files will be placed in the out directory when output="export" is enabled.
3. Verify the App Router pages are fully static
In the app directory, pages must not depend on request-time rendering. A safe static page looks like this:
export default function Page() {
return <main>Static content</main>
}
If you have dynamic routes, provide build-time params:
export async function generateStaticParams() {
return [
{ slug: 'one' },
{ slug: 'two' },
]
}
export default function Page({ params }) {
return <div>{params.slug}</div>
}
Without generateStaticParams, Next cannot prebuild required dynamic paths for a static export.
4. Remove server-dependent features from exported routes
Audit the app for patterns that break static export. Problematic examples include reading per-request data or forcing dynamic rendering:
import { headers } from 'next/headers'
export default function Page() {
const h = headers()
return <div>{h.get('host')}</div>
}
That code depends on a live request and is not suitable for pure export. Likewise, avoid:
export const dynamic = 'force-dynamic'
Prefer static fetching patterns that resolve at build time:
async function getData() {
const res = await fetch('https://example.com/data', {
cache: 'force-cache',
})
return res.json()
}
export default async function Page() {
const data = await getData()
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
5. Handle assets and links correctly for static hosting
Depending on the host, you may also need:
- trailingSlash: true for folder-based routing output
- basePath if deploying under a repository subdirectory
- assetPrefix for CDN or custom asset paths
Example:
const nextConfig = {
output: 'export',
basePath: '/ui',
trailingSlash: true,
images: {
unoptimized: true,
},
}
module.exports = nextConfig
6. Rebuild from a clean state
rm -rf .next out
yarn install
yarn build
Then inspect the generated out directory and deploy that folder to your static host.
7. Expected final workflow
For this issue, the corrected workflow is:
yarn install
yarn build
Not:
yarn install
yarn build
yarn next export
Common Edge Cases
Dynamic routes without generateStaticParams
If a route like app/posts/[slug]/page.js exists, a static export requires all slugs at build time. Missing generateStaticParams will cause export failure or missing pages.
Using next/image without unoptimized mode
The default image optimizer requires a running server. On static export, that usually breaks unless you add:
images: {
unoptimized: true,
}
Request-bound APIs in Server Components
Functions such as headers(), cookies(), or logic that depends on the incoming request prevent full static generation.
API routes or server actions expected at runtime
If the application depends on API routes hosted by Next itself, a static export cannot serve them. Move that logic to an external backend or deploy with a server-capable platform instead of pure export.
Deploying under a repository subpath
When hosting on GitHub Pages or another subdirectory-based host, asset URLs may 404 unless basePath is configured correctly.
Fetch behavior causing unintended dynamic rendering
Some data fetching patterns may opt into dynamic behavior depending on configuration. Ensure your fetches are compatible with build-time caching and static rendering.
FAQ
Can I use App Router with output="export"?
Yes, but only if the routes are fully static. Every page must be renderable at build time, and dynamic routes need generateStaticParams.
Why does next export fail after next build?
Because with output="export", the build already generates the static export output. Running next export afterward is redundant and can conflict with the App Router export flow.
What if my app needs cookies, headers, or server APIs?
Then a pure static export is the wrong deployment model. Use standard Next.js server deployment, or move request-dependent functionality behind an external API.
The practical fix for this GitHub issue is simple: keep output="export", remove the old next export command, and make sure every App Router page is truly static. Once those conditions are met, the App Router and static export can work together reliably.