📌 背景问题
在使用 Next.js App Router + 多语言(i18n)项目中,默认不会自动将 /a/b 跳转到 /zh/a/b 或 /en/a/b,导致出现 404(not-found)的问题。
📌 原因详解:
1.App Router 模式是基于文件系统路由的,每个页面都必须在 app/ 目录下有对应的目录结构;
- 例如你创建了
src/app/[locale]/a/b/page.tsx,那么 只会匹配/zh/a/b或/en/a/b这类路径; - 当你访问
/a/b(没有[locale]前缀)时,Next.js 无法找到对应的页面 → 抛出404 not-found。
2.Next.js 不会默认将 /a/b 自动转发到 /zh/a/b
- 它不会猜测用户想要什么语言;
- i18n 配置(如
i18n.locales和defaultLocale)只影响 构建时国际化支持,并不会做 URL 重写。
3.使用了 App Router 而不是 Pages Router
- 在旧的 Pages Router 中你可以通过
next.config.js的i18n配置来开启自动语言前缀; - 但 App Router 下,这种行为需要你手动实现(通过中间件、服务器逻辑、客户端跳转等)。
以下是四种方案对比
| 方案 | 描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| ① Fastify 中间件跳转(我现在的方案) | 在 server.ts 中拦截所有请求,未带 locale 的路径统一重定向 | ✅ 控制早,支持异步接口✅ 兼容 App Router✅ 可读 cookie、请求头等✅ 适合复杂业务逻辑 | ❌ 需自定义 server.ts,部署较重 | ✅ 适合 SSR、多语言、支持动态语言判断 |
② Next.js middleware.ts | 使用中间件做跳转逻辑,运行于 edge | ✅ 原生支持、无需自定义 server✅ 靠近框架层 | ❌ 无法访问接口(fetch 会被限制)❌ 不适合复杂逻辑(如多商户) | ✅ 静态多语言站点,语言写死在 cookie |
③ 页面级跳转(如 /a/b 渲染 LocaleRedirect) | 为每个路径建个 page.tsx,内部判断并跳转 | ✅ 不影响 SSR 架构,适合纯前端项目 | ❌ 每个路径都要建文件❌ 页面加载后跳转 → 闪烁 | ❌ 结构繁琐,已被淘汰,不推荐 |
| ④ Nginx 或 CDN 重写规则 | 在网关层判断路径并重定向 | ✅ 不进应用逻辑、跳转快 | ❌ 无法判断 cookie、header❌ 无法使用远程接口判断语言 | ✅ 静态站点或不依赖用户上下文 |
⑤ 在 layout.tsx 中使用 redirect()(App Router 特性) | layout 文件中判断 locale 并重定向 | ✅ 可访问 cookie/header,结构整洁 | ❌ 只能在 layout 层做❌ 页面已渲染一部分,跳转晚 | ✅ 对 SSR 不敏感、前后端同构简单项目 |
场景推荐
| 你的需求 | 推荐方案 |
|---|---|
| ✅ 使用 Fastify 接管 server.ts | ✅ 首选方案 ①(你已用) |
| ✅ 想让一切保持 Next 原生、无需服务端 | ✅ 可用方案 ②(middleware.ts) |
❌ 项目中路由已大量使用 /zh/... | ✅ 应用构建时预设 locale 前缀 |
| ✅ 不想让用户看到白屏或闪烁跳转 | ✅ 优先用服务端中间件(方案 ① / ②) |
| ❌ 动态读取语言的接口被限制 | ✅ 使用 cookie fallback 方式 |
| ✅ 静态页面部署(如 Netlify) | ✅ 可选方案 ④(Nginx 或 CDN 跳转) |
我的方案
我采用的是 “Fastify 插件 + 自定义服务”方案,这是目前在 **支持 SSR + 动态语言判断(如远程 /fePublicInfo)**的前提下最稳妥、灵活、强大的方案。
我的项目优先级以及处理是:
- 请求进入 Fastify;
setupLocaleRedirect拦截,判断 URL 是否缺 locale;- 语言来源优先级:
接口返回——fePublicInfo>cookie> 默认值; - 自动重定向;
- 后续再执行
Sentry、安全头、权限判断、中间件注册等。
具体实现方法
在server.ts注册最早的中间件:
await server.register(setupLocaleRedirect); // 👈 在所有路由处理前执行
核心代码逻辑(setupLocaleRedirect.ts):
server.addHook('onRequest', async (req, reply) => {
const url = req.url;
// ✅ 判断当前请求路径是否已经带了语言前缀(如 /zh /en)
const hasLocale = /^\/(zh|en)(\/|$)/.test(url);
// ✅ 忽略静态资源
const isStatic = url.startsWith('/_next') || url.startsWith('/favicon');
if (hasLocale || isStatic) return;
// ✅ 默认语言 fallback 为 en
let detectedLocale: 'zh' | 'en' = 'en';
// ✅ 尝试从 /fePublicInfo 接口中获取语言
try {
const response = await fetch('https://www.xxx.com/fePublicInfo', {
headers: {
cookie: req.raw.headers.cookie || '',
},
});
const data = await response.json();
if (data?.language === 'zh' || data?.language === 'en') {
detectedLocale = data.language;
}
} catch {}
// ✅ 若接口失败,则尝试从 cookie 取
const cookieLang = req.cookies?.lan;
if (!detectedLocale && (cookieLang === 'zh' || cookieLang === 'en')) {
detectedLocale = cookieLang;
}
// ✅ 写入 cookie,供后续前端读取
reply.setCookie('lan', detectedLocale, {
path: '/',
httpOnly: false,
});
// ✅ 真正的核心:重定向逻辑
const redirectTo = `/${detectedLocale}${url}`;
reply.redirect(302, redirectTo); // 👈 执行跳转
});
✅ 总结它解决的是什么问题?
| 场景 | 如果没有 setupLocaleRedirect | 有它之后 |
|---|---|---|
用户访问 /a/b | ❌ 报 404(因为没有 (no-locale)/a/b/page.tsx) | ✅ 自动 302 跳转到 /zh/a/b 或 /en/a/b |
用户访问静态资源 /favicon.ico | ✅ 正常 | ✅ 依然正常,不会被重定向 |
用户访问 /zh/a/b | ✅ 正常 | ✅ 正常,不触发重定向 |
| 判断语言方式 | 只能靠客户端/中间件读取 cookie | ✅ 优先用接口、然后 cookie |
✅ 为什么不用 Next.js middleware.ts 来跳转?
| 项目结构 | 是否推荐 |
|---|---|
| Next 原生中间件(middleware.ts) | ❌ 在 App Router 中功能受限,不支持读取接口 |
| Fastify 插件 + server.ts | ✅ 支持 header、cookie、远程请求、任意跳转逻辑 |