Max
Back to Blog

Next.js Static Export in Practice: One Codebase, Deployed to GitHub Pages and Vercel

My personal site is served from both GitHub Pages and Vercel, and both publish automatically from the same repo. The trick is configuring Next.js 15 (App Router) with output: "export", which produces a fully static out/ directory after build — no Node server, no serverless functions, deployable anywhere that can host static files.

Why a fully static export

A portfolio site has no need for runtime computation, and going fully static brings very practical benefits: plenty of free hosting options, straightforward CDN caching, and no server cold starts or costs to worry about. Meanwhile you keep the Next.js developer experience — components, Sass support, file-based routing — which beats hand-maintaining multiple HTML files by a mile.

The key configuration

Three settings in next.config.mjs matter: output: "export" enables static export; images.unoptimized: true, because the static build cannot use the Next image optimization server; and trailingSlash: true, so that a path like /zh resolves to /zh/index.html — without it, plain file servers like GitHub Pages will 404.

The Vercel pitfall

When Vercel detects a Next.js project it defaults to framework: "nextjs" mode and looks inside out/ for routes-manifest.json — a file that only exists in .next/. The result: a "successful" deployment that 404s on every page. The fix is an explicit vercel.json with framework: null, buildCommand: "next build", and outputDirectory: "out", so Vercel just runs the build and serves out/ as a plain static directory.

Other lessons learned

If the build throws a mysterious PageNotFoundError, it is usually a stale .next cache — rm -rf .next and rebuild. Also, browser-only APIs like WebGL and localStorage must be loaded with next/dynamic and ssr: false, or wrapped in useEffect, otherwise the server-side prerender of the static export will crash.

One codebase, two publishing channels, zero server cost — for a personal site or marketing page, I am very happy with this combination.