-
Notifications
You must be signed in to change notification settings - Fork 45
fix(seo): leetcode 中文 slug 旧 URL 301 到拼音新路径 #318
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # Leetcode 中文 slug 301 重定向 | ||
|
|
||
| ## 为什么需要这东西 | ||
|
|
||
| `lib/source.ts` 里的 transformer 会把 `app/docs/career/interview-prep/leetcode/` 下含中文的 | ||
| 文件名转成拼音 slug(例如 `2309兼具大小写的最好英文字母_translated.md` 对外变成 | ||
| `/docs/career/interview-prep/leetcode/2309-jian-ju-...-translated`)。 | ||
|
|
||
| 但历史上 Google Search Console 已经索引了**两批**旧 URL: | ||
|
|
||
| 1. `/docs/CommunityShare/Leetcode/<中文原文件名>` —— Option C IA 大重组前的旧路径 | ||
| 2. `/docs/CommunityShare/Leetcode/<拼音 slug>` —— 拼音化上线后被 Google 发现但还没编入 | ||
|
|
||
| `next.config.mjs` 里只写了一条 wildcard `/docs/CommunityShare/Leetcode/:path*` → | ||
| `/docs/career/interview-prep/leetcode/:path*`,**只做前缀替换不做 slug 拼音化**。 | ||
| 所以第 1 批 URL 跳到新路径之后 slug 还是中文,目标页依然 404。 | ||
|
|
||
| GSC 实测 41 条 404 全都是这个问题。 | ||
|
|
||
| ## 现在的方案 | ||
|
|
||
| **构建时生成字面映射表 + middleware O(1) 查表 301**。选型考虑: | ||
|
|
||
| - ❌ 直接在 `next.config.mjs` 里列出来 41 条:手写脆,文件增删没人同步 | ||
| - ❌ path-to-regexp wildcard 传参:空格 / `[]` / 中文在 Next.js 路由匹配里不稳 | ||
| - ❌ 在 middleware 里动态跑 `pinyin-pro`:整本字典(~1MB+)塞进 Edge bundle 太大 | ||
| - ✅ **构建时扫目录 + 输出 JSON,middleware 导入 JSON 查表**:Edge bundle 只多几 KB | ||
|
|
||
| ## 组件 | ||
|
|
||
| | 文件 | 作用 | | ||
| | ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | | ||
| | `scripts/generate-leetcode-slug-map.mjs` | 扫 leetcode 目录,对含中文的文件名跑和 `lib/source.ts` 一致的拼音化算法,输出 JSON | | ||
| | `generated/leetcode-slug-map.json` | 构建产物,`中文 stem → 拼音 stem` 的字面映射(当前 32 条) | | ||
| | `proxy.ts` (`redirectLeetcodeIfNeeded`) | middleware 在 `/docs/CommunityShare/Leetcode/*` 和 `/docs/career/interview-prep/leetcode/*` 上查表并 301 | | ||
| | `package.json` `prebuild` | 构建前自动跑生成脚本,保证 JSON 永远最新 | | ||
|
|
||
| ## 覆盖的请求形态 | ||
|
|
||
| | 输入 pathname | 查表结果 | 最终 301 → | | ||
| | --------------------------------------------------- | -------- | ---------------------------------------------------------------- | | ||
| | `/docs/CommunityShare/Leetcode/<中文 slug>` | 命中 | `/docs/career/interview-prep/leetcode/<拼音 slug>` | | ||
| | `/docs/CommunityShare/Leetcode/<ASCII slug>` | 未命中 | `/docs/career/interview-prep/leetcode/<ASCII slug>`(slug 原样) | | ||
| | `/docs/career/interview-prep/leetcode/<中文 slug>` | 命中 | 同目录拼音 slug | | ||
| | `/docs/career/interview-prep/leetcode/<ASCII slug>` | 未命中 | 放行(不动) | | ||
|
|
||
| ## 新增 / 重命名 leetcode 文件时 | ||
|
|
||
| 不需要做任何事。`pnpm build` 的 prebuild 会重跑脚本,JSON 自动同步。 | ||
|
|
||
| 但如果 **pinyin 规则本身要改**(例如 tone、分隔符),必须**同时**改两处: | ||
|
|
||
| 1. `lib/source.ts` 里的 `convertSlugToPinyin`(运行时给页面生成 slug) | ||
| 2. `scripts/generate-leetcode-slug-map.mjs` 里的 `convertSlugToPinyin`(构建时给 redirect 生成 slug) | ||
|
|
||
| 两者算法不同步 = 301 跳过去还是 404。 | ||
|
|
||
| ## 本地验证 | ||
|
|
||
| ```bash | ||
| pnpm build | ||
| pnpm start # 默认 3000 端口 | ||
| # 带中文的旧 URL | ||
| curl -I 'http://localhost:3000/docs/CommunityShare/Leetcode/2309%E5%85%BC%E5%85%B7%E5%A4%A7%E5%B0%8F%E5%86%99%E7%9A%84%E6%9C%80%E5%A5%BD%E8%8B%B1%E6%96%87%E5%AD%97%E6%AF%8D_translated' | ||
| # 期望:301,Location 指向拼音 slug | ||
| ``` | ||
|
|
||
| ## GSC 善后 | ||
|
|
||
| 这些 301 上线后,GSC 需要时间重抓: | ||
|
|
||
| - 41 条「未找到 (404)」:点「验证修复」→ GSC 会重新爬取,看到 301 就清出 404 列表 | ||
| - 36 条「已发现 - 尚未编入索引」:这批本来就是**新**拼音 URL,路径没错,只是 Google 还没排到抓取 —— 可以手动「请求编入索引」加速,但没有代码层面要改的 |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| { | ||
| "1234. 替换子串得到平衡字符串_translated": "1234-ti-huan-zi-chuan-de-dao-ping-heng-zi-fu-chuan-translated", | ||
| "142.环形链表II_translated": "142-huan-xing-lian-biao-iitranslated", | ||
| "1653. 使字符串平衡的最少删除次数_translated": "1653-shi-zi-fu-chuan-ping-heng-de-zui-shao-shan-chu-ci-shu-translated", | ||
| "1664生成平衡数组的方案数_translated": "1664-sheng-cheng-ping-heng-shu-zu-de-fang-an-shu-translated", | ||
| "1825求出 MK 平均值_translated": "1825-qiu-chu-mk-ping-jun-zhi-translated", | ||
| "1828统计一个圆中点的数目_translated": "1828-tong-ji-yi-ge-yuan-zhong-dian-de-shu-mu-translated", | ||
| "2131. 连接两字母单词得到的最长回文串": "2131-lian-jie-liang-zi-mu-dan-ci-de-dao-de-zui-chang-hui-wen-chuan", | ||
| "2299强密码检验器II_translated": "2299-qiang-mi-ma-jian-yan-qi-iitranslated", | ||
| "2309兼具大小写的最好英文字母_translated": "2309-jian-ju-da-xiao-xie-de-zui-hao-ying-wen-zi-mu-translated", | ||
| "2335. 装满杯子需要的最短总时长_translated": "2335-zhuang-man-bei-zi-xu-yao-de-zui-duan-zong-shi-chang-translated", | ||
| "2341. 数组能形成多少数对_translated": "2341-shu-zu-neng-xing-cheng-duo-shao-shu-dui-translated", | ||
| "2639. 查询网格图中每一列的宽度_translated": "2639-cha-xun-wang-ge-tu-zhong-mei-yi-lie-de-kuan-du-translated", | ||
| "2679.矩阵中的和_translated": "2679-ju-zhen-zhong-de-he-translated", | ||
| "2894. 分类求和并作差": "2894-fen-lei-qiu-he-bing-zuo-cha", | ||
| "3072. 将元素分配到两个数组中 II_translated": "3072-jiang-yuan-su-fen-pei-dao-liang-ge-shu-zu-zhong-iitranslated", | ||
| "345. 反转字符串中的元音字母_translated": "345-fan-zhuan-zi-fu-chuan-zhong-de-yuan-yin-zi-mu-translated", | ||
| "46.全排列": "46-quan-pai-lie", | ||
| "538.把二叉搜索树转换为累加树_translated": "538-ba-er-cha-sou-suo-shu-zhuan-huan-wei-lei-jia-shu-translated", | ||
| "6323. 将钱分给最多的儿童_translated": "6323-jiang-qian-fen-gei-zui-duo-de-er-tong-translated", | ||
| "76最小覆盖子串_translated": "76-zui-xiao-fu-gai-zi-chuan-translated", | ||
| "93复原Ip地址": "93-fu-yuan-ip-di-zhi", | ||
| "994.腐烂的橘子_translated": "994-fu-lan-de-ju-zi-translated", | ||
| "[121]买卖股票的最佳时期_translated": "121-mai-mai-gu-piao-de-zui-jia-shi-qi-translated", | ||
| "[1333]餐厅过滤器_translated": "1333-can-ting-guo-l-qi-translated", | ||
| "[146]LRU 缓存_translated": "146lru-huan-cun-translated", | ||
| "[1545]找出第 N 个二进制字符串中的第 K 位": "1545-zhao-chu-di-n-ge-er-jin-zhi-zi-fu-chuan-zhong-de-di-k-wei", | ||
| "[213]打家劫舍 II_translated": "213-da-jia-jie-she-iitranslated", | ||
| "[2490]回环句_translated": "2490-hui-huan-ju-translated", | ||
| "[2562]找出数组的串联值_translated": "2562-zhao-chu-shu-zu-de-chuan-lian-zhi-translated", | ||
| "[2582]递枕头_translated": "2582-di-zhen-tou-translated", | ||
| "brief_alternate 作业帮忙_translated": "briefalternate-zuo-ye-bang-mang-translated", | ||
| "剑指 Offer II 021. 删除链表的倒数第 n 个结点_translated": "jian-zhi-offerii021-shan-chu-lian-biao-de-dao-shu-di-n-ge-jie-dian-translated" | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { pinyin } from "pinyin-pro"; | ||
|
|
||
| /** | ||
| * 把 leetcode 目录下含中文的文件名 / slug 片段转成纯 ASCII 拼音 slug。 | ||
| * | ||
| * 为什么抽出来独立成文件: | ||
| * 1. 运行时 (`lib/source.ts` 里的 transformer) 要用它把 Fumadocs 预生成的 slugs 拼音化 | ||
| * 2. 构建时 (`scripts/generate-leetcode-slug-map.mts`) 要用它生成「中文 → 拼音」字面映射 | ||
| * 给 proxy.ts 做 301 查表 | ||
| * 两处算法必须完全一致,否则 301 跳过去找不到页面。复制粘贴迟早忘记同步, | ||
| * 因此抽成唯一真源。 | ||
| * | ||
| * 规则: | ||
| * - 无中文 → 原样返回(保留纯英文 slug 的连字符、数字等) | ||
| * - 有中文 → 拼音化 + 去掉所有非 `[a-z0-9]` 字符,再用 `-` 连接 | ||
| */ | ||
| export function convertSlugToPinyin(text: string): string { | ||
| // Fumadocs 内部的 slugs 可能被 encode 过(%E6%BC...),先 decode 再判断汉字 | ||
| const decodedText = decodeURIComponent(text); | ||
| if (!/[\u4e00-\u9fa5]/.test(decodedText)) return text; | ||
|
|
||
| return pinyin(decodedText, { | ||
| toneType: "none", | ||
| type: "array", | ||
| nonZh: "consecutive", | ||
| }) | ||
| .map((t) => t.toLowerCase().replace(/[^a-z0-9]/g, "")) | ||
| .filter(Boolean) | ||
| .join("-"); | ||
| } |
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /** | ||
| * 构建时扫描 app/docs/career/interview-prep/leetcode/*.md(x), | ||
| * 把「中文/含特殊字符的文件名」→「拼音 slug」的映射写进 generated/leetcode-slug-map.json。 | ||
| * | ||
| * 为什么要这个 map: | ||
| * lib/source.ts 里的 transformer 会把 leetcode 目录下含中文的文件名转成拼音 slug(对外 URL)。 | ||
| * GSC 旧索引里还存着 /docs/CommunityShare/Leetcode/<中文原文件名> 这类 URL, | ||
| * next.config.mjs 只做了前缀替换 wildcard,slug 没拼音化,跳过去还是 404。 | ||
| * proxy.ts (Next 16 middleware) 要在 edge 端 O(1) 查表把旧 URL 301 到正确拼音路径, | ||
| * 又不能把 pinyin-pro 的整本字典塞进 edge bundle,所以构建时先把映射固化成 JSON。 | ||
| * | ||
| * 算法从 lib/leetcode-slug.ts 里 import,运行时和构建时共用同一实现, | ||
| * 消除双点维护。脚本必须用 tsx 执行(见 package.json prebuild)。 | ||
| */ | ||
| import fs from "node:fs"; | ||
| import path from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { convertSlugToPinyin } from "../lib/leetcode-slug.ts"; | ||
|
|
||
| const __filename = fileURLToPath(import.meta.url); | ||
| const __dirname = path.dirname(__filename); | ||
| const PROJECT_ROOT = path.resolve(__dirname, ".."); | ||
| const LEETCODE_DIR = path.join( | ||
| PROJECT_ROOT, | ||
| "app/docs/career/interview-prep/leetcode", | ||
| ); | ||
| const OUTPUT_FILE = path.join(PROJECT_ROOT, "generated/leetcode-slug-map.json"); | ||
|
|
||
| /** | ||
| * 从文件名去掉 locale / 扩展名后缀,还原 Fumadocs 会当 slug 的 stem。 | ||
| * 2309兼具大小写的最好英文字母_translated.md → 2309兼具大小写的最好英文字母_translated | ||
| * 2241-design-an-atm-machine.zh.md → 2241-design-an-atm-machine | ||
| * [146]LRU 缓存_translated.md → [146]LRU 缓存_translated | ||
| */ | ||
| function stripSuffix(filename: string): string { | ||
| let stem = filename.replace(/\.(md|mdx)$/i, ""); | ||
| stem = stem.replace(/\.(en|zh)$/i, ""); | ||
| return stem; | ||
| } | ||
|
|
||
| function main() { | ||
| if (!fs.existsSync(LEETCODE_DIR)) { | ||
| console.error(`[leetcode-slug-map] 目录不存在: ${LEETCODE_DIR}`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const files = fs | ||
| .readdirSync(LEETCODE_DIR) | ||
| .filter((f) => /\.(md|mdx)$/i.test(f)); | ||
|
|
||
| const map: Record<string, string> = {}; | ||
| const collisions: { stem: string; existing: string; incoming: string }[] = []; | ||
|
|
||
| for (const file of files) { | ||
| const stem = stripSuffix(file); | ||
| const pinyinSlug = convertSlugToPinyin(stem); | ||
| if (pinyinSlug === stem) continue; // 无中文,不需要映射 | ||
| if (map[stem] && map[stem] !== pinyinSlug) { | ||
| collisions.push({ stem, existing: map[stem], incoming: pinyinSlug }); | ||
| } | ||
| map[stem] = pinyinSlug; | ||
| } | ||
|
|
||
| if (collisions.length) { | ||
| console.warn( | ||
| `[leetcode-slug-map] 检测到 slug 冲突 ${collisions.length} 条:`, | ||
| collisions, | ||
| ); | ||
| } | ||
|
|
||
| fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true }); | ||
| fs.writeFileSync(OUTPUT_FILE, JSON.stringify(map, null, 2) + "\n", "utf8"); | ||
|
|
||
| console.log( | ||
| `[leetcode-slug-map] 生成 ${Object.keys(map).length} 条映射 → ${path.relative(PROJECT_ROOT, OUTPUT_FILE)}`, | ||
| ); | ||
| } | ||
|
|
||
| main(); |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
这个是 Copilot 过时了——Next 16 把
middleware.ts官方改名为proxy.ts,导出符号也从middleware改成proxy。仓库里 commit 7e079c3 "fix: 修复警告, 现在已经用Proxy代替Middleware了" 就是做这次迁移。实测验证(production build +
next start):Build 输出里也有
ƒ Proxy (Middleware)那行,说明它被识别为 middleware。