Skip to content

Commit e981ff5

Browse files
docs(dev): seo meta description three-layer scheme
补 dev_docs/seo_meta_description.md,完整记录这次 SEO 治理的方案: 为什么需要、Layer 1/2/3/4 各自做什么、决策记录、验证步骤、相关文件。
1 parent 3593557 commit e981ff5

1 file changed

Lines changed: 157 additions & 0 deletions

File tree

dev_docs/seo_meta_description.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# SEO Meta Description 三层方案
2+
3+
## 为什么需要这东西
4+
5+
2026-05 Bing Webmaster Tools 报告:involutionhell.com 有 **118 个页面 meta description 太短**
6+
(< 150 字符),评为 Moderate SEO 问题。
7+
8+
根因不是代码 bug,是**内容缺失**
9+
10+
- docs 页面(`app/[locale]/docs/[...slug]/page.tsx:165`)的 `generateMetadata` 直接读 MDX
11+
frontmatter 的 `description` 字段,没有 fallback;
12+
- `content/docs/` 下 292 个 MDX 中:
13+
- **96 个** 完全没有 `description` 字段(绝大多数是程序化导入的 leetcode 题解)
14+
- **67 个** `description: ""` 空字符串
15+
- **35 个** description < 20 字符("First page" 这种)
16+
- **56 个** description 20-60 字符(合格但偏短)
17+
-**38 个** ≥ 60 字符
18+
19+
这条 Bing 告警不是 hard error,是 ranking signal —— description 缺失时搜索引擎会从正文
20+
随便抓一段做摘要,质量不可控,间接拉低 CTR 和排名。
21+
22+
## 方案:三层叠加
23+
24+
### Layer 1 — 代码层兜底(`lib/seo-description.ts`
25+
26+
所有 docs 页的 `generateMetadata` 都过 `ensureSeoDescription()`,把短/空/缺失的
27+
description 自动拼接到 ≥ 80 字符:
28+
29+
```ts
30+
description: ensureSeoDescription({
31+
description: page.data.description,
32+
title: page.data.title,
33+
sectionPath: slug.slice(0, -1), // 当前页的分区面包屑
34+
locale,
35+
});
36+
```
37+
38+
兜底文本结构:
39+
40+
```
41+
[作者原 description 如有] 主题:{title}。 所属分区:{breadcrumb}。 站点 tagline。
42+
```
43+
44+
中英文 tagline 各一份,按 `locale` 选。
45+
46+
**影响范围**:以下 4 处页面 metadata 都已接入:
47+
48+
| 文件 | 用途 |
49+
| ------------------------------------------ | ------------------------------------------------------- |
50+
| `app/[locale]/docs/[...slug]/page.tsx:165` | docs 动态路由 generateMetadata + TechArticle JSON-LD |
51+
| `app/[locale]/docs/page.tsx:47` | docs 根落地页 |
52+
| `app/[locale]/events/[id]/page.tsx:57` | 活动详情页(兜底用户/管理员录入的 `event.description`|
53+
| `app/[locale]/feed/page.tsx:24` | 社区分享墙 |
54+
55+
**作用**:立即消除 Bing 告警,不动 content/。但兜底文本是模板化拼接,
56+
搜索摘要质量稀薄 —— 这是 Layer 1 的局限。
57+
58+
### Layer 2 — CI lint 阻止再积累低质量 description
59+
60+
`scripts/check-frontmatter-description.mjs` 在 pre-commit + GitHub Actions PR
61+
检查新增/修改的 MDX,强制 `description` 字段 ≥ 60 字符。
62+
63+
豁免规则:
64+
65+
- `content/docs/career/interview-prep/leetcode/` 全部豁免 —— 程序化导入太多,
66+
Layer 1 兜底已能用
67+
- `*_translated.md` / `*_translated.mdx` —— 机翻产物,等人工 review 时再补
68+
69+
**接入位置**
70+
71+
- `.husky/pre-commit``pnpm check:frontmatter`(默认 `--changed` 模式)
72+
- `.github/workflows/content-check.yml`:同上,PR 上下文自动从 `GITHUB_BASE_REF`
73+
diff 找改动文件
74+
75+
**只看不阻塞模式**`pnpm check:frontmatter:all` 扫所有文件输出报表,但不退出
76+
非 0;用于本地一次性看现状。
77+
78+
### Layer 3 — 离线脚本回填 description 写入 frontmatter(`scripts/generate-descriptions.mjs`
79+
80+
把存量 253 个 description 缺失/空/极短的 MDX 回填精准描述。
81+
82+
两种生成策略:
83+
84+
1. **默认(推荐)**:所有文件走 DeepSeek API
85+
- 输入:title + filename + 正文前 800 字符(清洗 import / 代码块 / MDX 组件)
86+
- 输出:单行 description,中文文档 80-100 字、英文文档 120-160 字符
87+
- leetcode 题解会被额外提示"以 LeetCode {题号}. {题名} 题解 — 开头"
88+
- 成本:~$0.05 总 token 成本(DeepSeek 2026-05 价目)
89+
90+
2. **`--leetcode-only`(离线模式)**:仅 leetcode 走文件名+正文模板拼接,不调 LLM
91+
- 用于无 `DEEPSEEK_API_KEY` 时的 fallback
92+
- 模板覆盖:题号 + 题名 + 正文首句 + 通用 tail
93+
94+
**用法**
95+
96+
```bash
97+
# 1. dry-run(默认),生成 scripts/.descriptions-report.json 供 review
98+
DEEPSEEK_API_KEY=sk-xxx node scripts/generate-descriptions.mjs
99+
100+
# 2. 看 dry-run 结果后真写回 frontmatter
101+
DEEPSEEK_API_KEY=sk-xxx node scripts/generate-descriptions.mjs --apply
102+
103+
# 3. 试运行只跑前 N 个
104+
DEEPSEEK_API_KEY=sk-xxx node scripts/generate-descriptions.mjs --limit=5
105+
106+
# 4. 离线模式(leetcode 模板)
107+
node scripts/generate-descriptions.mjs --leetcode-only --apply
108+
```
109+
110+
**安全设计**
111+
112+
- 默认 dry-run,绝不动 content;`--apply` 才真写
113+
- `gray-matter` 保留其他 frontmatter 字段(title / date / docId / lang 等)
114+
- 已合格(≥ 60 字符)的 description 跳过 —— 重跑幂等
115+
- DeepSeek 调用失败的不写,留待重跑
116+
- 输出 < 50 字符的不写,标记在 report 的 `skippedTooShort`
117+
- API key 只从 env 读,不进任何 commit
118+
119+
## 验证
120+
121+
```bash
122+
# 看 Layer 1 兜底是否在跑:访问随便一个无 description 的 leetcode 页面
123+
curl -s https://involutionhell.com/zh/docs/career/interview-prep/leetcode/1004... | grep '<meta name="description"'
124+
# 期望:description 至少包含 "主题:" 或 "Topic:" + 分区面包屑 + 站点 tagline
125+
126+
# 看 Layer 2 lint 是否触发:本地建一个无 description 的 mdx
127+
echo "---\ntitle: x\n---\n内容" > content/docs/__test.mdx
128+
git add content/docs/__test.mdx
129+
pnpm check:frontmatter # 预期退出码 1
130+
131+
# Layer 3 dry-run,看 report
132+
DEEPSEEK_API_KEY=sk-xxx node scripts/generate-descriptions.mjs --limit=5
133+
cat scripts/.descriptions-report.json | jq '.results[] | {file, length, after}'
134+
```
135+
136+
## 决策记录
137+
138+
- **为什么 lint 阈值是 60 字符而非 Bing 推荐的 150**:60 是保守值,照顾老贡献者
139+
适应;Layer 1 兜底会把渲染时实际长度补到 ≥ 80。两道防线足够。
140+
- **为什么 leetcode 豁免 lint**:96 个题解是程序化导入的,没人会手写
141+
description;强制写人为操作太重,靠 Layer 1 兜底 + Layer 3 模板/LLM 一次性补齐
142+
即可。
143+
- **为什么用 DeepSeek 而非更贵的模型**:本任务对模型推理要求不高(看 800 字
144+
正文摘要成一句话),DeepSeek-chat 足够;成本 $0.05 vs GPT-4 的 $5+,差 100 倍。
145+
- **为什么不直接在 source.config.ts 装 remark 插件自动生成 description**
146+
fumadocs 已经把 frontmatter 转为 `page.data`,在 metadata 层加 fallback 比改
147+
frontmatter pipeline 侵入性小、回退安全。
148+
149+
## 相关文件
150+
151+
- `lib/seo-description.ts` — Layer 1 兜底函数
152+
- `tests/seo-description.test.ts` — 兜底函数单元测试
153+
- `scripts/check-frontmatter-description.mjs` — Layer 2 lint
154+
- `scripts/generate-descriptions.mjs` — Layer 3 离线生成
155+
- `app/[locale]/docs/[...slug]/page.tsx` — 主要消费方
156+
- `.husky/pre-commit` — 接入点
157+
- `.github/workflows/content-check.yml` — 接入点

0 commit comments

Comments
 (0)