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.
Max Chu