Skip to content

Commit cf656b6

Browse files
docs(dev): 补 i18n URL 段化架构说明 + content/ 目录 README
dev_docs/i18n_url_routing.md(217 行): - 为什么从 cookie 切到 URL 段(CPU 死结的来龙去脉) - 文件分工:i18n/ + proxy.ts + app/[locale]/ + content/docs/ - SSG 开关:每个 page/layout 必须 setRequestLocale + docs 加 force-static - 文档命名约定(dot parser 规则 + 加新文章 / 缺译 fallback) - 切换语言流程 + SEO(hreflang / canonical / sitemap / robots) - proxy 流程图 + 调试 5 类常见问题("为什么 page 还是 ƒ") - 已知未做的下一轮工作清单(首页 SSG 等) content/README.md: - 说明 content/docs 是 fumadocs mdx 内容根(与 app/ 路由分离) - 命名约定快速版(避免新人放 .zh.mdx 触发 build 冲突) - 历史:2026-05 从 app/docs 拆出
1 parent aaada4f commit cf656b6

2 files changed

Lines changed: 267 additions & 0 deletions

File tree

content/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# content/
2+
3+
文档内容(mdx)的根目录。和 `app/` 下的路由文件分离 —— 路由怎么渲染由
4+
`app/[locale]/docs/[...slug]/page.tsx` 决定,这里只放被渲染的内容。
5+
6+
## 子目录
7+
8+
- `docs/` — 全部社区文档。fumadocs 在 build 时从这里递归扫 `.md` / `.mdx`
9+
自动生成 PageTree(左侧 sidebar 用)。
10+
11+
## 命名约定
12+
13+
参见 `dev_docs/i18n_url_routing.md` 的「文档命名约定」章节。简单版:
14+
15+
- `xxx.mdx` → 中文(默认 locale)
16+
- `xxx.en.mdx` → 英文翻译
17+
- 不要用 `xxx.zh.mdx`(与无后缀冲突,fumadocs build 会报错)
18+
19+
## 历史
20+
21+
原 mdx 内容混在 `app/docs/` 下(路由 + 内容混居)。2026-05 i18n URL 段化
22+
改造时分离到这里,符合 fumadocs 推荐的 routes / content 分离布局。
23+
GitHub Edit URL(见 `lib/github.ts``DOCS_BASE`)必须与本目录路径一致。

dev_docs/i18n_url_routing.md

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# i18n URL 段化架构
2+
3+
## 为什么是这套
4+
5+
社区 2026-04-16 上线的双语方案是「URL 不变 + cookie 决定语言」:
6+
7+
```ts
8+
// 旧版 i18n/request.ts
9+
export default getRequestConfig(async () => {
10+
const cookieStore = await cookies();
11+
const locale = cookieStore.get("locale")?.value === "en" ? "en" : "zh";
12+
return { locale, messages };
13+
});
14+
```
15+
16+
`cookies()` 在 RSC 里调用 → next-intl 把这个 hook 注入到整棵 RSC 树 →
17+
**全站所有 page 都被钉成 dynamic**。318 篇 docs 每访问一次现 SSR 一次:
18+
19+
- `pnpm build` 表里所有 user-facing 路由都是 `ƒ Dynamic`
20+
- `.next/prerender-manifest.json` 只剩 5 条预渲染(robots / search.json / sitemap)
21+
- Vercel Observability 显示 30 天 Fluid Active CPU 用了 3h 51m / 4h(Hobby 96%)
22+
- 流量越大 CPU 越涨,不可持续
23+
24+
2026-05 改成 next-intl 标准的 **URL routing** —— locale 从 URL 段(`/zh/...` /
25+
`/en/...`)推断,不读 cookie,全树可静态化。
26+
27+
## 文件分工
28+
29+
```
30+
i18n/
31+
routing.ts defineRouting:locales [zh, en], localePrefix: always
32+
navigation.ts createNavigation:Link / useRouter / usePathname / redirect
33+
request.ts getRequestConfig:从 requestLocale 读,不再 await cookies()
34+
35+
proxy.ts (Next.js 16 用 proxy.ts 不是 middleware.ts)
36+
1. 老 leetcode 中文 slug 优先 301
37+
2. 其它请求交给 next-intl createMiddleware
38+
39+
app/
40+
layout.tsx 极简 root layout:html/body + 全局 metadata + 全局 script
41+
(theme inline / structured data / GA / Umami)
42+
不读 locale,不包 NextIntlClientProvider
43+
[locale]/
44+
layout.tsx 调 setRequestLocale + 包所有 provider
45+
(NextIntlClientProvider / ThemeProvider / AuthProvider /
46+
fumadocs RootProvider)
47+
inline script 把 documentElement.lang 改到当前 locale
48+
page.tsx 首页(仍 ƒ,下一轮迁)
49+
docs/
50+
page.tsx /[locale]/docs landing
51+
layout.tsx docs sidebar layout(用 source.getPageTree(locale))
52+
[...slug]/
53+
page.tsx 文章详情,force-static + setRequestLocale + 双倍预渲染
54+
admin/ 管理后台也并入 [locale],写死 zh 也行(messages 里有)
55+
events/ feed/ login/ rank/ settings/ share/ editor/ u/
56+
57+
api/ 不进 [locale],无 UI 的 fetch endpoint
58+
sitemap.ts 输出双语 + alternates.languages
59+
robots.ts disallow 用 wildcard /\*\/admin/ 等
60+
61+
content/
62+
docs/ ← 这里是 mdx 内容(旧版混在 app/docs/,已分离)
63+
fumadocs 推荐 routes / content 分离
64+
```
65+
66+
## SSG 的开关
67+
68+
**关键开关:每个 page 和 layout 都得调 `setRequestLocale(locale)`**。少一个,
69+
对应路由就被 next-intl 退回 dynamic。
70+
71+
```tsx
72+
// app/[locale]/<some-route>/page.tsx
73+
import { setRequestLocale } from "next-intl/server";
74+
import { hasLocale } from "next-intl";
75+
import { notFound } from "next/navigation";
76+
import { routing } from "@/i18n/routing";
77+
78+
export default async function Page({
79+
params,
80+
}: {
81+
params: Promise<{ locale: string }>;
82+
}) {
83+
const { locale } = await params;
84+
if (!hasLocale(routing.locales, locale)) notFound();
85+
setRequestLocale(locale); // ← 必须,且必须在任何 next-intl hook 之前
86+
87+
// ... 业务逻辑
88+
}
89+
```
90+
91+
### docs 详情页额外加 `force-static`
92+
93+
```tsx
94+
// app/[locale]/docs/[...slug]/page.tsx
95+
export const dynamic = "force-static";
96+
```
97+
98+
不加这条时,即便 `setRequestLocale` 都对了,Next.js 16 仍会把这条路由
99+
`ƒ Dynamic`(实测 SSG 0 页)。`force-static` 让 Next 严格按
100+
`generateStaticParams` 预渲染所有 (locale, slug) 组合。
101+
102+
### generateStaticParams 双倍出货
103+
104+
```tsx
105+
export async function generateStaticParams() {
106+
return source.generateParams("slug", "lang").map((p) => ({
107+
locale: p.lang as string,
108+
slug: p.slug as string[],
109+
}));
110+
}
111+
```
112+
113+
fumadocs 自带的 `generateParams('slug', 'lang')` 会按 i18n 配置自动产出
114+
{locale × slug} 笛卡尔积。我们 mapping 一下 lang→locale(next-intl 用 locale)。
115+
116+
## 文档命名约定
117+
118+
`source.config.ts` 用 fumadocs **dot parser**
119+
120+
| 文件名 | 识别为 |
121+
| ------------------- | -------------------------------------- |
122+
| `xxx.mdx`(无后缀) | zh(默认) |
123+
| `xxx.en.mdx` | en |
124+
| `xxx.zh.mdx` | **不要用**,与无后缀冲突,build 会报错 |
125+
126+
历史上仓库里有 8 对 conflict(无后缀 = 英文 + `.zh.mdx` = 中文翻译),改造时
127+
统一 swap 成「无后缀 = zh + `.en.mdx` = 英文翻译」。
128+
129+
### 加新文章
130+
131+
```bash
132+
# 中文原文(默认 locale)
133+
content/docs/learn/<分区>/<新文章>.mdx
134+
135+
# 英文翻译(可选)
136+
content/docs/learn/<分区>/<新文章>.en.mdx
137+
```
138+
139+
frontmatter 不需要写 `lang` 字段。fumadocs 按文件名后缀识别。
140+
141+
### 缺译怎么办
142+
143+
`lib/source.ts``fallbackLanguage: "zh"`:访问 `/en/docs/<slug>``.en.mdx`
144+
不存在时,自动渲染原文(zh)。文档站合理体验(缺译显示中文 > 显示空白)。
145+
146+
## 切换语言
147+
148+
`<LocaleToggle />` 用 next-intl 的 `useRouter().replace(pathname, { locale })`
149+
原理:
150+
151+
1. `usePathname()` 返回去 locale 的 pathname(例如当前 URL 是 `/zh/docs/x`
152+
pathname = `/docs/x`
153+
2. `router.replace(pathname, { locale: "en" })` 自动加 `/en` 前缀 →
154+
实际跳转 `/en/docs/x`
155+
3. next-intl 同时把 `NEXT_LOCALE` cookie 同步到 `en`,供下次访问根路径
156+
`/` 时 middleware 选默认 locale 用
157+
158+
切换是真 URL 跳转,浏览器历史会留两条记录(`router.replace` 只覆盖当前记录)。
159+
160+
## SEO
161+
162+
### hreflang / canonical
163+
164+
`docs/[...slug]/page.tsx``generateMetadata` 输出:
165+
166+
```
167+
canonical: /<locale>/docs/<slug>
168+
languages:
169+
zh-CN: /zh/docs/<slug>
170+
en-US: /en/docs/<slug>
171+
x-default: /zh/docs/<slug>
172+
```
173+
174+
每个 locale 各自 canonical,避免 zh / en 互相竞争 PageRank。
175+
176+
### sitemap
177+
178+
`app/sitemap.ts` 每个 URL 输出双语 entry,并填 `alternates.languages`。Google
179+
`xhtml:link rel="alternate" hreflang` 元素建立两个 URL 之间的关系,正确处理
180+
搜索结果按用户语言展示。
181+
182+
### robots
183+
184+
`disallow` 用 wildcard 形式 `/*/admin/` 等匹配两种 locale 前缀。
185+
186+
## proxy 流程
187+
188+
每个请求 → `proxy.ts`
189+
190+
1. 命中老 leetcode 路径 → 301 到拼音新 URL(带 locale 前缀)
191+
2. 否则 → next-intl `createMiddleware`
192+
- 不带 locale 前缀 → 308 redirect 到 `/<defaultLocale>/...`(可能是 `/zh`
193+
或按 cookie / Accept-Language)
194+
- 带 locale 前缀 → 直通
195+
196+
matcher 排除 `api / trpc / _next / _vercel / 任何带 . 的路径`。admin 不再排除
197+
(已并入 `[locale]/admin`)。
198+
199+
## 调试常见问题
200+
201+
### 添加新 page 后 build 表里仍是 `ƒ Dynamic`
202+
203+
90% 是 page 或它的 layout 缺 `setRequestLocale`。先在 page 里加,再看 layout
204+
有没有调。`setRequestLocale` 必须排在任何 next-intl hook(`useTranslations` /
205+
`getMessages` / `getTranslations`)之前。
206+
207+
### 加了 `setRequestLocale` 还是 `ƒ`
208+
209+
可能 page 或 layout 调了 server fetch(例如 `await fetch(BACKEND_URL)`)。
210+
任何 server fetch 都会让该路由 dynamic。要么改成 client fetch,要么 build 时
211+
prebuild 到 JSON(参考 `generated/site-leaderboard.json` 模式)。
212+
213+
### `Both middleware file and proxy file detected`
214+
215+
Next.js 16 只接受 `proxy.ts`,不再接受 `middleware.ts`。两个文件不能共存。
216+
217+
### docs sidebar 缺翻译版变体
218+
219+
老版用手写 `pickVariantsByLocale` 剪 sidebar tree。新版用 `source.getPageTree(locale)`,fumadocs i18n 已经按 locale 隔离,不再需要手写过滤。`SectionIndex` 组件
220+
`docs/layout.tsx` 都靠这套。
221+
222+
### Edit on GitHub 链接 404
223+
224+
`DOCS_BASE``lib/github.ts` 应是 `content/docs`。如果改了 mdx 路径,要
225+
同步改这个常量 + `lib/contributors.ts``normalizeRelativePath`
226+
227+
## 已知未做(下一轮 PR)
228+
229+
| 路由 | 当前 | 阻碍 | 处理思路 |
230+
| ------------------------ | ---- | ------------------------------------------ | ----------------------------------------- |
231+
| `/[locale]` | ƒ | `await fetchHomepageEvents()` server fetch | FloatWindow 自己 client fetch;首页变 SSG |
232+
| `/[locale]/events` | ƒ | server fetch backend | client component 化;或 ISR 化 |
233+
| `/[locale]/feed` | ƒ | server fetch | 同上 |
234+
| `/[locale]/u/[username]` | ƒ | 用户数据是 dynamic | 保持 dynamic 即可(量小) |
235+
| `/[locale]/admin/*` | ƒ | 鉴权页面,不需要 SSG | 保持 dynamic |
236+
237+
实际**只有首页值得做**(占当前 CPU 25%)。其它要么数据动态,要么访问量小。
238+
239+
## 参考
240+
241+
- next-intl URL routing 标准 setup: <https://next-intl-docs.vercel.app/docs/routing>
242+
- fumadocs i18n: <https://fumadocs.dev/docs/headless/internationalization>
243+
- 老 cookie 方案的 commit: `d0a420d` (2026-04-16 i18n 双语系统初版)
244+
- URL 段化改造的 PR: #330

0 commit comments

Comments
 (0)