返回博客

Next.js 静态导出子路由全 403,根源是 trailingSlash 没开

把一个 Next.js App Router 项目设成 output: "export" 静态导出,本地 next build 一切正常,rsync 到服务器,首页打开,心情愉快。然后点导航栏里的「博客」——403。点「项目」——403。只有首页是好的。

你去检查 nginx 配置,看起来也没问题。直接 curl https://example.com/blog.html ——200,内容也对。但站内链接都是 /blog,不是 /blog.html,用户怎么可能手敲后缀?

现象

  • GET / → 200(正常)
  • GET /blog → 301 → /blog/403
  • GET /blog/403
  • GET /blog.html → 200(带后缀反而能开)
  • out/ 目录里:有 blog.html,有 blog/ 目录,但 blog/ 里只有 Next.js 内部的 RSC payload 文件,没有 index.html

根因

Next.js output: "export" 的默认导出形态是扁平 html

out/
  index.html          ← 首页,nginx 对 / 能命中
  blog.html           ← /blog 页,但 nginx 不认
  blog/               ← 这个目录里没有 index.html!
    _next.*.txt       ← RSC payload,不是页面
  projects/
    detail.html
    detail/           ← 同上,只有 payload

nginx 处理 GET /blog 的逻辑是:看到同名目录 blog/ 存在,先发 301 重定向到 /blog/,再对目录请求查找 index index.html,目录里没有 → 403(autoindex 默认关闭)。

/blog.html 能开,是因为 nginx try_files $uri 直接命中了文件,不经过目录逻辑。

坑:同名目录的存在触发了 nginx 目录重定向

out/blog/ 目录是 Next.js 放 RSC payload 用的,不是页面目录。但 nginx 看到 blog/ 目录存在,就把 GET /blog 301 到 /blog/,再找 index.html,找不到 → 403。你看 nginx 配置没问题是对的——问题在导出文件结构,不在服务器。

正解

next.config 加一行 trailingSlash: true

// next.config.mjs
const config = {
  output: "export",
  trailingSlash: true,   // 改变导出结构
};
 
export default config;

重新 next build 后,导出结构变成:

out/
  index.html
  blog/
    index.html    ← 这才是页面
  projects/
    detail/
      index.html

nginx 对 GET /blog/ 能直接命中 blog/index.html,对 GET /blog 发 301 到 /blog/ 再命中,全程 200。Next.js 的 <Link> href 也会自动规范成带斜杠形式,站内跳转一致。

重新部署时用 rsync --delete 会自动删掉旧的 blog.html,不会留下新旧双份文件。

正解:trailingSlash: true 让导出结构与静态服务器期望对齐

加了 trailingSlash: true 之后,页面以 路由/index.html 形式导出,这正是 nginx/Apache/任何静态托管平台的通用期望格式。不需要改服务器配置,换任何静态托管都通用。

备选:只改服务器不改导出

如果不想动导出结构(比如多人协作、改 next.config 有别的影响),可以在 nginx location 加 .html 回退:

location / {
    root   /var/www/out;
    index  index.html;
    try_files $uri $uri.html $uri/ =404;
}

try_files 会依次尝试 $uri(精确文件)→ $uri.html(加后缀)→ $uri/(目录),命中 blog.html 就返回,不再走目录逻辑。

两选一即可,前者更稳

trailingSlash: true 是代码侧修复,部署到 Vercel、GitHub Pages、Cloudflare Pages 等任何平台都无需额外配置;try_files 是服务器侧修复,换平台时要记得同步改。优先选前者。

一句话外卖

next export 默认产 route.html,静态服务器需要的是 route/index.html;加 trailingSlash: true 让导出形态与服务器期望对齐,一行配置消灭所有子路由 403。