Skip to content

refactor(i18n): URL 段化让 docs 全量 SSG,砍 Vercel Fluid CPU 50%+#330

Merged
longsizhuo merged 2 commits intomainfrom
refactor/i18n-url-segments
May 6, 2026
Merged

refactor(i18n): URL 段化让 docs 全量 SSG,砍 Vercel Fluid CPU 50%+#330
longsizhuo merged 2 commits intomainfrom
refactor/i18n-url-segments

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

背景

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

  • `i18n/routing.ts` defineRouting (locales: zh/en, localePrefix: always)
  • `i18n/navigation.ts` createNavigation 出 `Link` / `useRouter` / `usePathname` / `redirect`
  • `i18n/request.ts` 改读 `requestLocale`,删 `await cookies()`

2. 路由重排

  • 17 个 user-facing page + admin 整体迁到 `app/[locale]/...`
  • `app/api/*` / `app/sitemap.ts` / `app/robots.ts` 保留在根
  • `app/layout.tsx` 极简化:html/body + 全局 metadata + theme script + structured data
  • `app/[locale]/layout.tsx` 调 `setRequestLocale` + 包所有 provider

3. fumadocs i18n

  • `lib/source.ts` 接入 `defineI18n`(dot parser, fallbackLanguage zh)
  • mdx 内容从 `app/docs` 拆到 `content/docs`(路由文件 / 内容分离,符合 fumadocs 推荐)
  • normalize 8 对 conflict(无后缀=英文 + .zh.mdx=中文 → 重命名为无后缀=zh、.en.mdx=英文)
  • `docs/[...slug]/page.tsx`: `source.getPage(slug, locale)` + `force-static` + `setRequestLocale`
  • `SectionIndex` 简化 60+ 行(删手写翻译版剪枝,靠 fumadocs i18n 的 pageTree 自动按 locale 隔离)

4. SEO + 切换

  • `LocaleToggle` 走 next-intl `router.replace + locale`
  • `sitemap.ts` 输出双语 entry + `alternates.languages`(hreflang 自动)
  • `robots.ts` disallow 用 wildcard 匹配两种语言
  • canonical / hreflang 在 docs page generateMetadata 重新生成

5. proxy 合并 (Next.js 16)

  • 删旧 IP geo + Accept-Language + cookie 写入逻辑(next-intl 原生支持)
  • 老 leetcode 中文 slug 301 redirect 保留,先于 i18n middleware 跑

6. 路径统一

  • `DOCS_BASE` / `contributors` normalize / `api/chat` fs.readFile / 4 个 scripts 全部 `app/docs` → `content/docs`

验证

```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)

预期收益

  • Vercel Fluid CPU docs 这块归零(占当前 50%+)
  • TTFB 下降(docs 直 serve 静态 HTML,不经 Function)
  • 国际 SEO 提升(zh / en 独立 URL,独立索引权重)

已知未做(下一轮 PR)

  • 首页 (`/[locale]`) 仍 ƒ —— `fetchHomepageEvents` 是 server fetch backend;要改为 FloatWindow client 自己 fetch(占 25% CPU)
  • `/[locale]/events` / `/feed` / `/u/[username]` 等仍 ƒ —— 每个 page 单独加 `setRequestLocale` 才能 SSG,但占 CPU 比例小

影响面 / 风险

  • URL 全变:所有 `/docs/x` → `/zh/docs/x` 或 `/en/docs/x`。next-intl middleware 会把不带 locale 的老 URL 308 redirect 到默认 locale
  • Google 索引:发布后 1-2 周内 Search Console 会看到大量 redirect,期间收录波动正常
  • Giscus 评论:按 pathname 绑定,老评论仍挂在 `/docs/x` URL 下;redirect 后能跟着新 URL 走(giscus mapping=pathname 默认追新 URL)
  • fumadocs 文件命名:8 对 conflict normalize 过,规则统一为「无后缀=zh,.en.mdx=英文」

Test plan

  • preview 部署后访问 `/zh/docs/learn/ai/rl` 看到中文版,`/en/...` 看到英文版
  • LocaleToggle 切换从 `/zh/...` 跳到 `/en/...` 同一 pathname,反之
  • 老 URL `/docs/learn/ai/rl` 自动 redirect 到 `/zh/docs/learn/ai/rl`
  • 老 leetcode 中文 slug 301 跳转仍生效(`/docs/CommunityShare/Leetcode/<中文>`)
  • sitemap.xml 包含双语 entry + `xhtml:link rel="alternate" hreflang`
  • Vercel preview build log 里 `/[locale]/docs/[...slug]` 标记 `●`

主因: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
Copilot AI review requested due to automatic review settings May 6, 2026 14:00
@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
involutionhell-github-io Ready Ready Preview, Comment May 6, 2026 2:18pm
website-preview Ready Ready Preview, Comment May 6, 2026 2:18pm

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

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 拆出
@longsizhuo longsizhuo merged commit e74b5fd into main May 6, 2026
8 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants