Next.js 静态导出(output: export)踩坑记录
把 Next.js 项目做成纯静态站点(输出 HTML + 资源文件,丢到任何静态服务器都能跑)是很常见的需求,但 output: 'export' 会关掉一批服务端能力,踩坑点集中在配置和构建。
基础配置
// next.config.ts
const nextConfig = {
output: process.env.NODE_ENV === "production" ? "export" : undefined,
trailingSlash: true,
images: { unoptimized: true },
};
output: 'export'只在生产构建时开启,开发时保留完整能力trailingSlash: true让每个页面输出成目录/index.html,这样 Nginx、Apache、GitHub Pages 都不需要额外配置就能访问images.unoptimized: true是必须的,静态导出没有图片优化服务
坑一:rewrites / redirects / headers 完全不生效
构建时会看到警告:
⚠ Specified "rewrites" will not automatically work with "output: export"
这不是提示,是事实——静态导出没有服务端,这些配置一条都不会执行。需要跳转或重写,只能:
- 用服务器配置(Nginx
try_files、Apache.htaccess) - 用 Cloudflare Rules / Pages Functions
- 在客户端用 JS 兜底(不推荐用于 SEO 相关跳转)
本站的 www → 非 www 跳转就是三管齐下:Cloudflare 规则 + .htaccess 的 301 + 页面内联脚本兜底。
坑二:动态路由必须 generateStaticParams
export function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}
少写这一步,构建会直接报错。dynamicParams: true 在静态导出下也不可用——所有页面必须在构建时确定。
坑三:Route Handler 只能是静态 GET
静态导出支持 app/xxx/route.ts,但限制很明确:
- 只能用
GET - 不能依赖
Request的参数(query、header、cookie) - 返回值会在构建时算好,写成一个静态文件
所以可以用它生成 RSS:
export const dynamic = "force-static";
export function GET() {
const posts = getAllPosts();
return new Response(buildXml(posts), {
headers: { "Content-Type": "application/rss+xml; charset=utf-8" },
});
}
但不能用它做登录、表单提交、数据库读写这类接口。本站的评论用 giscus、下载数据走 Supabase 直连,就是为了绕开这个限制。
坑四:sitemap 的生成时机
用 next-sitemap 时,它默认写 public/,而 public/ 会在构建时被复制到 out/。结果就是这次构建的 sitemap 里永远没有这次新增的页面,要等下一次构建才补上。
解决办法是直接让它写构建产物目录:
module.exports = {
siteUrl: "https://example.com",
outDir: "out",
};
并且确保 npm 脚本是:
{
"scripts": {
"build": "next build && next-sitemap"
}
}
如果只跑 npx next build,out/ 里根本不会有 sitemap.xml,而 robots.txt 还指着它——搜索引擎抓到的就是 404。
坑五:所有页面的 lastmod 都一样
next-sitemap 的 autoLastmod 默认是 true,会用构建时间覆盖你在 transform 里返回的值。要按文章真实日期输出,必须显式关闭:
module.exports = {
autoLastmod: false,
transform: async (config, path) => ({
loc: path,
lastmod: getPostDate(path) || new Date().toISOString(),
changefreq: "monthly",
priority: 0.8,
}),
};
另外 transform 收到的 path 不带结尾斜杠,用它做 map 查询时要把两种形式都登记,否则永远查不到。
还有一点:不要在 transform 里 return { ...config, ... },那会把 autoLastmod、exclude 这些内部字段序列化进 sitemap。
坑六:404 页面
静态导出会生成 404.html,但服务器需要配置才知道怎么用它:
error_page 404 /404.html;
Apache 用 .htaccess:
ErrorDocument 404 /404.html
不做这一步,访问不存在的路径会看到服务器默认的 404,体验和品牌都不对。
坑七:客户端组件不能导出 metadata
一个页面如果顶部写了 "use client",就不能同时 export const metadata。常见错误是「页面需要交互,于是整个文件加了 use client」,结果 SEO 信息全没了。
正确做法是把交互部分拆成子组件,页面本身保持服务端组件:
// app/downloads/page.tsx(服务端)
import DownloadList from "@/components/downloads/DownloadList";
export const metadata = { title: "下载" };
export default function Page() {
return <DownloadList />;
}
检查清单
out/里是否有sitemap.xml、feed.xml、404.html- 所有页面链接是否以
/结尾(配合trailingSlash) robots.txt里的 sitemap 地址是否可访问- 图片路径是否都是绝对路径(
/images/x.jpg) - 是否有依赖
rewrites的路由,部署端是否补了等价配置
常见问题
静态导出能做搜索功能吗?
可以,但要么在构建时生成索引文件由前端搜索,要么接第三方搜索服务(如 Algolia)。
部署到 Cloudflare Pages 还是自己服务器?
两者都行。Pages 免费、自带 CDN 和 HTTPS;自己的服务器可控性更强,配合 Cloudflare 也能获得同样效果。
为什么本地 next start 正常,上传后就 404?
next start 跑的是 Node 服务,而静态服务器只认文件。检查是否真的上传了 out/ 目录,以及服务器是否正确处理目录默认页。
能不能只在生产环境导出?
可以,用 process.env.NODE_ENV 判断即可,开发时保留完整能力更方便调试。