refactor(i18n): URL 段化让 docs 全量 SSG,砍 Vercel Fluid CPU 50%+#330
Merged
longsizhuo merged 2 commits intomainfrom May 6, 2026
Merged
refactor(i18n): URL 段化让 docs 全量 SSG,砍 Vercel Fluid CPU 50%+#330longsizhuo merged 2 commits intomainfrom
longsizhuo merged 2 commits intomainfrom
Conversation
主因:next-intl 原方案用 cookie + RSC 切语言(i18n/request.ts 调 cookies()),
让全站 RSC 树都被钉成 dynamic,318 篇 docs 每次访问现 SSR 一次,30 天 Vercel
Fluid Active CPU 月用量逼近 4h(Hobby 上限)。
改造:
1. next-intl 切到 URL routing
- i18n/routing.ts (defineRouting) localePrefix: always
- i18n/navigation.ts (createNavigation 出 Link/router/redirect)
- i18n/request.ts 从 requestLocale 读 locale,不再 await cookies()
2. 路由重排
- 17 个 user-facing page + admin 全部移到 app/[locale]/...
- app/api/* / app/sitemap.ts / app/robots.ts 保留在根
- app/layout.tsx 极简化(不读 cookies;只剩 html/body + 全局 metadata
+ theme inline script + structured data + analytics)
- app/[locale]/layout.tsx 调 setRequestLocale + 包所有 provider
(NextIntlClientProvider / ThemeProvider / AuthProvider / fumadocs
RootProvider)
3. fumadocs i18n
- lib/source.ts defineI18n: zh/en, dot parser, fallbackLanguage zh
- mdx 内容从 app/docs 拆到 content/docs(路由文件 / 内容分离,符合
fumadocs 推荐)
- normalize 8 对 conflict(无后缀=英文 + .zh.mdx=中文翻译 swap 成
无后缀=zh、.en.mdx=英文翻译,单一规则)
- app/[locale]/docs/[...slug]/page.tsx 用 source.getPage(slug, locale)
+ setRequestLocale + force-static
- SectionIndex 简化(删手写翻译版剪枝,靠 fumadocs i18n 的 pageTree
按 locale 隔离)
4. SEO + 切换
- LocaleToggle 改 next-intl router.replace + locale prefix
- sitemap.ts 输出双语 entry + alternates.languages(hreflang 自动)
- robots.ts disallow 用 wildcard 匹配两种语言
- canonical / hreflang 在 docs page generateMetadata 重新生成
5. proxy 合并 (Next.js 16 用 proxy.ts 不是 middleware.ts)
- 删掉旧 IP geo + Accept-Language + cookie 写入逻辑(next-intl
createMiddleware 原生支持)
- 老 leetcode 中文 slug 301 redirect 保留,先于 i18n middleware 跑
6. 路径统一
- DOCS_BASE / contributors normalize / api/chat fs.readFile / scripts
全部 app/docs → content/docs
预期:
- /[locale]/docs/[...slug] 从 ƒ Dynamic 变 ● SSG,build 时 322 静态页面
预渲染
- Vercel Fluid CPU docs 这块归零(占当前 50%)
- 首页 / events / feed 等仍 ƒ(fetchHomepageEvents 等 server fetch 阻挡,
下一轮单独 PR 处理)
验证:
- pnpm exec tsc --noEmit: 0 错误
- pnpm build: 通过
- .next/prerender-manifest.json: 322 routes prerendered
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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 拆出
3 tasks
longsizhuo
added a commit
that referenced
this pull request
May 6, 2026
…#331) * fix(ci): workflow path 跟随 i18n URL 段化(app/docs → content/docs) i18n PR (#330) merge 后,sync-uuid workflow 在 main 上 fail: git add 'app/docs/**/*.md' 'app/docs/**/*.mdx' 因为 app/docs 已经 不存在,pathspec 不匹配任何文件,git 退出码 128 → workflow 失败。 backfill 脚本本身 OK(已成功生成 JSON),失败在最后 commit 那一步。 修: 1. sync-uuid.yml - paths trigger: app/docs/** → content/docs/** - git diff / git add 路径同步改 content/docs 2. content-check.yml - paths trigger: app/docs/** → content/docs/** 3. deploy.yml (IndexNow 推送) - 删掉旧 app/docs/(.*)/page.tsx 提取分支(不再有 page.tsx-as-content) - content/docs/(.*).mdx 提 base slug 时剥离 locale 后缀(.en/.zh), 拿到 canonical slug - 每篇文档推送 zh + en 两条 URL(/zh/docs/<slug> + /en/docs/<slug>), i18n 段化后这是两个独立 URL,IndexNow 要分别通知 - fallback URL 也改成双语 ($SITE_ORIGIN/zh + $SITE_ORIGIN/en) 注:generated/doc-contributors.json 留给 sync-uuid workflow 修好后 下次跑自己 commit,不进本 PR。 * fix(ci): IndexNow 提 leetcode slug 时走拼音映射,与实际路由对齐 CR (Copilot) 指出 #331 的 deploy.yml 直接拿文件路径当 slug,但 lib/source.ts 的 transformer 把 career/interview-prep/leetcode/ 下 含中文的文件名拼音化(convertSlugToPinyin): 文件:content/docs/career/interview-prep/leetcode/142.环形链表II_translated.md 实际路由:/<locale>/docs/career/interview-prep/leetcode/142-huan-xing-lian-biao-iitranslated 我之前推的:/<locale>/docs/career/interview-prep/leetcode/142.环形链表II_translated ← 404 修:在 deploy.yml 的 IndexNow URL 提取里复用 generated/leetcode-slug-map.json (prebuild 时由 scripts/generate-leetcode-slug-map.mts 用同一份算法生成), 对 leetcode 子树的 slug 做 stem → 拼音 映射后再推送。 非 leetcode 子树的 slug 不受影响(它们的文件名都是 ASCII,路由按 file path 直出,不需要映射)。 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This was referenced May 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
Vercel Observability 显示近 30 天 Fluid Active CPU 用了 3h 51m / 4h(Hobby 上限 96%),快撞墙。Functions 表里
/docs/[...slug]占 1.2K invocations(占全站请求一半),首页 821 invocations。本地 build 验证
/docs/[...slug]是ƒ Dynamic—— 318 篇文档每访问一次现 SSR 一次。根因
i18n/request.ts在 RSC 里await cookies()读 locale。next-intl 把这个函数注入到整棵 RSC 树,所有 page 都被钉成 dynamic:```ts
// 旧版(让全站 dynamic)
export default getRequestConfig(async () => {
const cookieStore = await cookies(); // ← 全站每个 RSC 都跑
const locale = cookieStore.get("locale")?.value === "en" ? "en" : "zh";
return { locale, messages: ... };
});
```
前后端分离没解决问题,因为 docs 渲染本身就是 Function CPU,跟 Java 后端无关。
方案
切到 next-intl 标准的 URL routing —— locale 从 URL 段(`/zh/...` / `/en/...`)推断,不读 cookie,全树可静态化。
1. next-intl URL routing
2. 路由重排
3. fumadocs i18n
4. SEO + 切换
5. proxy 合并 (Next.js 16)
6. 路径统一
验证
```text
$ pnpm exec tsc --noEmit
(0 errors)
$ pnpm build
✓ Compiled successfully
✓ Generating static pages (359/359) in 8.4s
Route (app)
├ ● /[locale]/docs/[...slug] ← 从 ƒ 变 ●
│ ├ /zh/docs/career
│ ├ /zh/docs/community
│ ├ /zh/docs/learn
│ └ [+314 more paths]
```
`.next/prerender-manifest.json`: 322 routes prerendered(含全部 docs zh + en)
预期收益
已知未做(下一轮 PR)
影响面 / 风险
Test plan