|
| 1 | +/** |
| 2 | + * 构建时扫描 app/docs/career/interview-prep/leetcode/*.md(x), |
| 3 | + * 把「中文/含特殊字符的文件名」→「拼音 slug」的映射写进 generated/leetcode-slug-map.json。 |
| 4 | + * |
| 5 | + * 为什么要这个 map: |
| 6 | + * lib/source.ts 里的 transformer 会把 leetcode 目录下含中文的文件名转成拼音 slug(对外 URL)。 |
| 7 | + * GSC 旧索引里还存着 /docs/CommunityShare/Leetcode/<中文原文件名> 这类 URL, |
| 8 | + * next.config.mjs 只做了前缀替换 wildcard,slug 没拼音化,跳过去还是 404。 |
| 9 | + * proxy.ts (Next 16 middleware) 要在 edge 端 O(1) 查表把旧 URL 301 到正确拼音路径, |
| 10 | + * 又不能把 pinyin-pro 的整本字典塞进 edge bundle,所以构建时先把映射固化成 JSON。 |
| 11 | + * |
| 12 | + * 生成规则必须和 lib/source.ts 的 convertSlugToPinyin 完全一致,否则链接对不上。 |
| 13 | + */ |
| 14 | +import fs from "node:fs"; |
| 15 | +import path from "node:path"; |
| 16 | +import { fileURLToPath } from "node:url"; |
| 17 | +import { pinyin } from "pinyin-pro"; |
| 18 | + |
| 19 | +const __filename = fileURLToPath(import.meta.url); |
| 20 | +const __dirname = path.dirname(__filename); |
| 21 | +const PROJECT_ROOT = path.resolve(__dirname, ".."); |
| 22 | +const LEETCODE_DIR = path.join( |
| 23 | + PROJECT_ROOT, |
| 24 | + "app/docs/career/interview-prep/leetcode", |
| 25 | +); |
| 26 | +const OUTPUT_FILE = path.join(PROJECT_ROOT, "generated/leetcode-slug-map.json"); |
| 27 | + |
| 28 | +/** |
| 29 | + * 与 lib/source.ts 中 convertSlugToPinyin 保持同步。 |
| 30 | + * 入参:单个 slug 片段(一般是文件名 stem)。 |
| 31 | + * 无中文直接原样返回;有中文则按拼音 + 非字母数字清洗 + 连字符拼接。 |
| 32 | + */ |
| 33 | +function convertSlugToPinyin(text) { |
| 34 | + const decodedText = decodeURIComponent(text); |
| 35 | + if (!/[\u4e00-\u9fa5]/.test(decodedText)) return text; |
| 36 | + return pinyin(decodedText, { |
| 37 | + toneType: "none", |
| 38 | + type: "array", |
| 39 | + nonZh: "consecutive", |
| 40 | + }) |
| 41 | + .map((t) => t.toLowerCase().replace(/[^a-z0-9]/g, "")) |
| 42 | + .filter(Boolean) |
| 43 | + .join("-"); |
| 44 | +} |
| 45 | + |
| 46 | +/** |
| 47 | + * 从文件名去掉 locale / 扩展名后缀,还原 Fumadocs 会当 slug 的 stem。 |
| 48 | + * 2309兼具大小写的最好英文字母_translated.md → 2309兼具大小写的最好英文字母_translated |
| 49 | + * 2241-design-an-atm-machine.zh.md → 2241-design-an-atm-machine |
| 50 | + * [146]LRU 缓存_translated.md → [146]LRU 缓存_translated |
| 51 | + */ |
| 52 | +function stripSuffix(filename) { |
| 53 | + let stem = filename.replace(/\.(md|mdx)$/i, ""); |
| 54 | + stem = stem.replace(/\.(en|zh)$/i, ""); |
| 55 | + return stem; |
| 56 | +} |
| 57 | + |
| 58 | +function main() { |
| 59 | + if (!fs.existsSync(LEETCODE_DIR)) { |
| 60 | + console.error(`[leetcode-slug-map] 目录不存在: ${LEETCODE_DIR}`); |
| 61 | + process.exit(1); |
| 62 | + } |
| 63 | + |
| 64 | + const files = fs |
| 65 | + .readdirSync(LEETCODE_DIR) |
| 66 | + .filter((f) => /\.(md|mdx)$/i.test(f)); |
| 67 | + |
| 68 | + const map = {}; |
| 69 | + const collisions = []; |
| 70 | + |
| 71 | + for (const file of files) { |
| 72 | + const stem = stripSuffix(file); |
| 73 | + const pinyinSlug = convertSlugToPinyin(stem); |
| 74 | + if (pinyinSlug === stem) continue; // 无中文,不需要映射 |
| 75 | + if (map[stem] && map[stem] !== pinyinSlug) { |
| 76 | + collisions.push({ stem, existing: map[stem], incoming: pinyinSlug }); |
| 77 | + } |
| 78 | + map[stem] = pinyinSlug; |
| 79 | + } |
| 80 | + |
| 81 | + if (collisions.length) { |
| 82 | + console.warn( |
| 83 | + `[leetcode-slug-map] 检测到 slug 冲突 ${collisions.length} 条:`, |
| 84 | + collisions, |
| 85 | + ); |
| 86 | + } |
| 87 | + |
| 88 | + fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true }); |
| 89 | + fs.writeFileSync(OUTPUT_FILE, JSON.stringify(map, null, 2) + "\n", "utf8"); |
| 90 | + |
| 91 | + console.log( |
| 92 | + `[leetcode-slug-map] 生成 ${Object.keys(map).length} 条映射 → ${path.relative(PROJECT_ROOT, OUTPUT_FILE)}`, |
| 93 | + ); |
| 94 | +} |
| 95 | + |
| 96 | +main(); |
0 commit comments