Next.js 中文路径 404 问题完整排查与解决指南
嗯... I love AI generated contents
1. 问题现象
- 本地开发环境 (
localhost:3000) 访问/这里是一个中文url正常。 - 部署到 Vercel 后,同一路径返回 404。
- Vercel 构建日志却显示该页面已被成功预渲染。
2. 根因分析
2.1 Unicode 规范化差异
| 平台 | 文件系统 | 默认规范化 | 说明 |
|---|---|---|---|
| macOS | APFS/HFS+ | NFD(分解形) | “我” ⇒ U+6211 + U+0323 |
| Linux/Vercel | ext4 | NFC(组合形) | “我” ⇒ U+6211 |
同一个字符在人眼看来一致,底层字节却不同,导致比较失败。
2.2 slug 生成与比较不一致
- 构建阶段(本地)使用 NFD 文件名生成 slug;
- 运行阶段(服务器)读取到 NFC 文件名,与请求中的 slug(可能是编码后的
%E6%88%91...,也可能是 NFD)进行比较; fileSlug !== slug⇒notFound()⇒ 404。
3. 解决方案
- 统一规范化:在所有处理文件名/slug 的地方调用
normalize('NFC')。 - 运行期解码:
const decodedSlug = decodeURIComponent(slug).normalize('NFC'); - 生成链接时编码:
const postUrl = `/${category}/${encodeURIComponent(slug.normalize('NFC'))}`; - 去掉
output: 'export'(可选),避免静态导出模式的潜在限制。
关键代码片段
// generateStaticParams
const slug = file
.normalize('NFC')
.replace(/^\d{4}-\d{2}-\d{2}-/, '')
.replace(/\.md$/, '')
.replace(/ /g, '-');
// getPostContent 比较
const fileSlug = f.normalize('NFC').replace(...);
const decodedSlug = decodeURIComponent(slug).normalize('NFC');
return fileSlug === decodedSlug;
4. 经验教训
- 跨平台开发要考虑 Unicode 规范化,特别是文件名涉及非 ASCII 字符时。
- slug 的「生成 → 链接 → 匹配」必须走同一套转换链。
- 遇到“日志中有路由但线上 404”的场景,多半是运行期读取/比较逻辑与构建期不一致。