Next.js 静态导出部署到子路径 (Next.js Static Export under a Sub-path)
目标:把一个 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-test4. 上传并落位
产物文件多且碎,用 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-profile | 301 → /3d-profile/ | |
| 页面 | curl -sI <URL>/3d-profile/ | 200 text/html | |
| 静态资源 | 取页面里的 css/js 路径再 curl | 200,且 content_type 正确 | |
| 图片/视频 | curl -sI <URL>/3d-profile/skills/x.png | 200 | |
| 内容 | `curl -s | 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()之类会在构建那一刻冻结,不重新构建就不会变。
相关
- 6767 主页加静态子页 (Add a Static Subpage to the 6767 Homepage) —— 纯静态站的子页套路,本文是它的框架版
- Nginx 静态资源缺失 mjs 映射导致 ESM 严格 MIME 校验拦截 (Nginx Missing mjs MIME Type Breaking ESM Module Scripts) —— 静态资源 MIME 类问题
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/nodejs