Next.js 中文路径 404 问题完整排查与解决指南

嗯... I love AI generated contents

1. 问题现象

  • 本地开发环境 (localhost:3000) 访问 /这里是一个中文url 正常。
  • 部署到 Vercel 后,同一路径返回 404
  • Vercel 构建日志却显示该页面已被成功预渲染。

2. 根因分析

2.1 Unicode 规范化差异

平台文件系统默认规范化说明
macOSAPFS/HFS+NFD(分解形)“我” ⇒ U+6211 + U+0323
Linux/Vercelext4NFC(组合形)“我” ⇒ U+6211

同一个字符在人眼看来一致,底层字节却不同,导致比较失败。

2.2 slug 生成与比较不一致

  1. 构建阶段(本地)使用 NFD 文件名生成 slug;
  2. 运行阶段(服务器)读取到 NFC 文件名,与请求中的 slug(可能是编码后的 %E6%88%91...,也可能是 NFD)进行比较;
  3. fileSlug !== slugnotFound() ⇒ 404。

3. 解决方案

  1. 统一规范化:在所有处理文件名/slug 的地方调用 normalize('NFC')
  2. 运行期解码
    const decodedSlug = decodeURIComponent(slug).normalize('NFC');
    
  3. 生成链接时编码
    const postUrl = `/${category}/${encodeURIComponent(slug.normalize('NFC'))}`;
    
  4. 去掉 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”的场景,多半是运行期读取/比较逻辑与构建期不一致。

5. 参考链接