目标:把一个 Next.js App Router 项目导出成纯静态文件,挂在既有域名的一个子路径下(例如 https://6767.chat/3d-profile/),由 nginx 直接托管,不需要常驻 node 进程。

最终形态:本地 npx next build 产出 out/,原样丢进站点静态根的一个子目录即可访问。

前置条件

  • Next.js App Router 项目,且没有用到需要服务端的能力(API Route、Server Action、动态渲染、revalidate)。
  • 站点的 nginx 有 location / { root <静态根>; index index.html; } 这样的兜底块——子目录会被自动覆盖,不需要为新子路径加配置。

步骤

1. 配置 next.config.js

const nextConfig = {
  output: "export",          // 产出 out/,不是 .next/
  basePath: "/3d-profile",   // 子路径,构建时写死进客户端包
  trailingSlash: true,       // 每路由产出 index.html,nginx 好找
  images: { unoptimized: true },  // 没有图片优化服务,直接用原图
};

basePath 的值在构建时会被内联进客户端 bundle,改了必须重新构建,不能靠改 nginx 补救。

2. 手动给所有 /public 资源加前缀

basePath 会自动作用于 next/link 的跳转和 _next/ 静态资源,但不会自动作用于:

  • next/image 的 src
  • 原生 <video><source src>
  • <img>、CSS 里写的绝对路径

漏掉任何一个,那一项就是 404 而页面其余部分正常——容易误判成"图片挂了"。

统一收口到一个 helper:

// config/index.ts
export const BASE_PATH = "/3d-profile";
export const asset = (path: string) => `${BASE_PATH}${path}`;

然后所有引用处走 asset("/videos/bg.webm")、asset("/skills/x.png")。

3. 构建与本地验证

npx next build     # 产出 out/

本地验证要模拟真实路径,否则测不出前缀问题:把 out/ 放进一个临时目录的 3d-profile/ 子目录里,再对该目录起静态服务,访问 /3d-profile/。

mkdir -p serve-test/3d-profile && cp -r out/* serve-test/3d-profile/
python -m http.server 3101 --directory serve-test

4. 上传并落位

产物文件多且碎,用 tar 走 ssh 比 scp 快:

cd out && tar czf - . | ssh tx 'rm -rf /tmp/x && mkdir -p /tmp/x && tar xzf - -C /tmp/x'

静态根通常属 www-data,当前用户不可写,落位要 sudo:

ssh tx 'DEST=<静态根>/3d-profile
sudo rm -rf "$DEST" && sudo mkdir -p "$DEST"
sudo cp -r /tmp/x/. "$DEST"/
sudo chown -R www-data:www-data "$DEST"
sudo find "$DEST" -type d -exec chmod 755 {} \;
sudo find "$DEST" -type f -exec chmod 644 {} \;'

验证方法

四层都要过:

层命令期望
路径归一curl -sI <URL>/3d-profile301 → /3d-profile/
页面curl -sI <URL>/3d-profile/200 text/html
静态资源取页面里的 css/js 路径再 curl200,且 content_type 正确
图片/视频curl -sI <URL>/3d-profile/skills/x.png200
内容`curl -s /3d-profile/ \grep <特征串>`命中

内容层用模板自带的假名做反向断言最省事——例如原模板里是 "John Doe",导出后 grep -c "John Doe" 必须为 0。

常见失败点

  • 只改了页面文案,忘了资源前缀:页面 200 但图标/视频全 404。检查构建后的 HTML 里有没有 src="/xxx" 这种不带前缀的裸路径。
  • 以为要动 nginx:有兜底 location / 的站点,子目录放进去就出页,加 location 块反而可能因为匹配顺序把资源挡掉。
  • 本地用根路径测:python -m http.server --directory out 直接访问 / 是能开的,但前缀 bug 完全测不出来。
  • next/font/google 构建时拉不到字体:构建机需要能访问 Google Fonts,走代理。
  • 品牌图标素材:simple-icons 新版会下架部分商标图标(OpenAI、Nginx 等),去 cdn.jsdelivr.net/npm/simple-icons@13/icons/ 取旧版;另外 Rust、Next.js 的品牌色是纯黑,深色底上要按亮度换成浅灰,否则图标隐形。

适用边界

  • 只适用于纯前端页面。一旦用到 API Route、Server Action、revalidate、运行时读取 header/cookie,就不能静态导出——那种情况得改成 output: "standalone" 跑 node 进程 + nginx 反代。
  • trailingSlash: true 会让 /foo 重定向到 /foo/,如果站点其他部分依赖无斜杠 URL,注意保持一致。
  • 静态导出意味着内容在构建时定死。页脚里的 new Date().getFullYear() 之类会在构建那一刻冻结,不重新构建就不会变。

相关


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/nodejs