Back to home

Static Export & Middleware

Explains the relationship between static export (output: 'export') and src/middleware.ts, and the static replacements.

Static Export (output: 'export') and src/middleware.ts

TL;DR

output: 'export' (static export) does not support middleware. To enable static export you must remove src/middleware.ts and hand its responsibilities (locale validation, prefix stripping, root redirect) over to generateStaticParams, setRequestLocale, localePrefix: 'always', and an nginx redirect.

What middleware does in normal mode

In non-static mode, src/middleware.ts uses next-intl's createMiddleware(routing) for i18n routing. It has three core jobs:

  1. Locale detection & validation — derive the language from the URL, 404 for invalid locales.
  2. Prefix stripping (as-needed) — with localePrefix: 'as-needed', the default locale (e.g. zh) has no prefix; middleware treats /zh/project-config and /project-config as the same route at runtime.
  3. Root redirect — redirect / to the default-locale path.

Key point: all of this happens dynamically in the Node edge runtime.

Why static export can't run middleware

  • output: 'export' turns next build output into pure static files (out/) served directly by nginx — there is no Node runtime.
  • Middleware runs in the edge/server runtime and needs request-time execution. There is no place for that logic in static files.
  • So Next.js ignores (or errors on) middleware when output: 'export' is set.

The fundamental conflict: as-needed vs static export

Even keeping middleware wouldn't help, because localePrefix: 'as-needed' itself conflicts with static export:

  • as-needed relies on the runtime to strip the default locale's prefix, making /project-config equivalent to /zh/project-config.
  • Static export pre-generates routes via generateStaticParams, which only emits prefixed pages (/en/*, /zh/*).
  • The unprefixed / and /project-config are never generated → they 404.

Verified: after generateStaticParams returns ['en', 'zh'], out/ contains only en/ and zh/ directories — no root index.html.

Static-export replacements (responsibility map)

Middleware responsibilityStatic-export replacement
Locale validation (404)hasLocale + notFound() in [locale]/layout.tsx
Prefix stripping (as-needed)switch to localePrefix: 'always'
Pre-generate routesgenerateStaticParams() in [locale]/layout.tsx
Root / redirectnginx location = / { return 301 /zh/; }
Mark locale for static renderingsetRequestLocale(locale)

Checklist to switch the build to static export

In react-next-admin:

  1. Add output: "export" (optionally trailingSlash: true) to next.config.ts.
  2. Delete src/middleware.ts.
  3. Change localePrefix: 'as-needed' to 'always' in src/i18n/routing.ts.
  4. Wrap useSearchParams() usages in <Suspense> (required for static prerendering).
  5. Keep generateStaticParams + setRequestLocale + hasLocale in [locale]/layout.tsx.
  6. Let nginx handle the root redirect at deploy time.

Note: after removing middleware, next start no longer applies; preview out/ with any static server (e.g. npx serve out).