Skip to content

Commit 66605b1

Browse files
authored
Merge pull request #341 from InvolutionHell/feat/seo-meta-description
feat(seo): fix 118 short meta descriptions reported by Bing
2 parents dc97591 + a53f2dc commit 66605b1

298 files changed

Lines changed: 4233 additions & 170 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/content-check.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,10 @@ jobs:
7676

7777
- name: Lint image references (non-blocking)
7878
run: pnpm lint:images || echo "[warn] image lint found issues (non-blocking)"
79+
80+
# Block PR if newly added/modified MDX is missing a proper description.
81+
# Old files are grandfathered via lib/seo-description.ts (Layer 1 fallback);
82+
# this check only fires on changed files in the PR (uses GITHUB_BASE_REF diff).
83+
# leetcode/ and _translated.md are exempt — see scripts/check-frontmatter-description.mjs
84+
- name: Check MDX frontmatter description
85+
run: pnpm check:frontmatter

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,6 @@ AGENTS.md
6262

6363
.npmrc
6464
/.env.*
65+
66+
# scripts/generate-descriptions.mjs 的 dry-run / apply 报表(产物,不进 git)
67+
scripts/.descriptions-report.json

.husky/pre-commit

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,11 @@ pnpm test || exit 1
1111
pnpm check:pnpm-version || true
1212
pnpm check:lockfile || true
1313

14-
# 5) 其余按 lint-staged 处理(如 Prettier)
14+
# 5) 校验新增/修改的 docs MDX 必须有 description(>= 60 字符)
15+
# Bing 2026-05 报告 118 个页面 description 太短,老内容由 lib/seo-description.ts
16+
# 代码层兜底,但新增/修改必须手写,避免再积累低质量 SEO 内容。
17+
# leetcode/ 和 _translated 自动豁免,详见 scripts/check-frontmatter-description.mjs
18+
pnpm check:frontmatter || exit 1
19+
20+
# 6) 其余按 lint-staged 处理(如 Prettier)
1521
pnpm exec lint-staged

app/[locale]/docs/[...slug]/page.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { source } from "@/lib/source";
22
import { safeJsonLdString } from "@/lib/json-ld";
33
import { SITE_URL } from "@/lib/site-url";
4+
import { ensureSeoDescription } from "@/lib/seo-description";
45
import { DocsPage, DocsBody } from "fumadocs-ui/page";
56
import { notFound } from "next/navigation";
67
import type { Metadata } from "next";
@@ -65,12 +66,23 @@ export default async function DocPage({ params }: Param) {
6566
? `${SITE_URL}/${locale}/docs/${slugPath}`
6667
: `${SITE_URL}/${locale}/docs`;
6768

69+
// JSON-LD description 同步走兜底:避免结构化数据里出现空字符串,否则
70+
// Google Rich Results 测试会 warning。与 generateMetadata 里的逻辑一致。
71+
const sectionPathForJsonLd =
72+
(slug ?? []).length > 1 ? (slug ?? []).slice(0, -1) : [];
73+
const articleDescription = ensureSeoDescription({
74+
description: page.data.description,
75+
title: page.data.title,
76+
sectionPath: sectionPathForJsonLd,
77+
locale,
78+
});
79+
6880
// TechArticle: 让 docs 在 Google 搜索结果上更可能展示为技术文章卡片
6981
const articleJsonLd = {
7082
"@context": "https://schema.org",
7183
"@type": "TechArticle",
7284
headline: page.data.title,
73-
description: page.data.description,
85+
description: articleDescription,
7486
url: docUrl,
7587
inLanguage: locale === "en" ? "en-US" : "zh-CN",
7688
publisher: {
@@ -190,21 +202,35 @@ export async function generateMetadata({ params }: Param): Promise<Metadata> {
190202
"",
191203
);
192204

205+
// SEO description 兜底:page.data.description 可能为 undefined/空/极短
206+
// (96 个 leetcode 题解完全没 description,67 个空,35 个 < 20 字符)。
207+
// 用 ensureSeoDescription 拼 title + 面包屑 + 站点 tagline 补到 80+ 字符,
208+
// 让 Bing/Google 拿到完整摘要而不是从正文随便抓一段。
209+
// sectionPath 取 slug 除末段外的所有段(末段是当前页本身,已在 title 里)。
210+
const slugArr = slug ?? [];
211+
const sectionPath = slugArr.length > 1 ? slugArr.slice(0, -1) : [];
212+
const safeDescription = ensureSeoDescription({
213+
description: page.data.description,
214+
title: page.data.title,
215+
sectionPath,
216+
locale,
217+
});
218+
193219
return {
194220
title: page.data.title,
195-
description: page.data.description,
221+
description: safeDescription,
196222
alternates: { canonical, languages: langs },
197223
openGraph: {
198224
type: "article",
199225
title: page.data.title,
200-
description: page.data.description,
226+
description: safeDescription,
201227
url: canonical,
202228
locale: locale === "en" ? "en_US" : "zh_CN",
203229
},
204230
twitter: {
205231
card: "summary_large_image",
206232
title: page.data.title,
207-
description: page.data.description,
233+
description: safeDescription,
208234
},
209235
};
210236
}

app/[locale]/docs/page.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { hasLocale } from "next-intl";
55
import { notFound } from "next/navigation";
66
import { SectionIndex } from "@/app/components/docs/SectionIndex";
77
import { routing } from "@/i18n/routing";
8+
import { ensureSeoDescription } from "@/lib/seo-description";
89

910
/**
1011
* /[locale]/docs 根路由的 landing。Header 的 "文档 / Docs" 链接指到 /docs,
@@ -49,11 +50,17 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
4950
if (!hasLocale(routing.locales, locale)) notFound();
5051
setRequestLocale(locale);
5152

53+
// 走统一兜底:原文本只 ~60 字符,被 Bing 判定为太短。ensureSeoDescription
54+
// 会自动补足到 80+ 字符,并保持中英分别的 tagline。
5255
return {
5356
title: locale === "en" ? "Docs" : "文档",
54-
description:
55-
locale === "en"
56-
? "Involution Hell community knowledge base — AI, CS, jobs, community shares."
57-
: "Involution Hell 社区知识库 — AI、计算机基础、求职、群友分享等分区总览。",
57+
description: ensureSeoDescription({
58+
description:
59+
locale === "en"
60+
? "Involution Hell community knowledge base — AI, CS, jobs, community shares."
61+
: "Involution Hell 社区知识库 — AI、计算机基础、求职、群友分享等分区总览。",
62+
title: locale === "en" ? "Docs" : "文档",
63+
locale,
64+
}),
5865
};
5966
}

app/[locale]/events/[id]/page.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Footer } from "@/app/components/Footer";
66
import type { EventDetailResponse, EventView } from "../types";
77
import { InterestButton } from "./InterestButton";
88
import { sanitizeExternalUrl, sanitizeMediaUrl } from "@/lib/url-safety";
9+
import { ensureSeoDescription } from "@/lib/seo-description";
910

1011
/**
1112
* /events/[id] 详情页。SSR 拉 /api/events/{id}。
@@ -57,10 +58,26 @@ interface Param {
5758
export async function generateMetadata({ params }: Param): Promise<Metadata> {
5859
const { id } = await params;
5960
const data = await fetchDetail(id);
60-
if (!data) return { title: `活动 #${id} · Involution Hell` };
61+
if (!data) {
62+
// 没拿到 event 也兜底 description(404 前的过渡态)
63+
return {
64+
title: `活动 #${id} · Involution Hell`,
65+
description: ensureSeoDescription({
66+
title: `活动 #${id}`,
67+
sectionPath: ["events"],
68+
locale: "zh",
69+
}),
70+
};
71+
}
72+
// event.description 由用户/管理员录入,长度不可控;走兜底防短。
6173
return {
6274
title: `${data.event.title} · Involution Hell`,
63-
description: data.event.description || "Involution Hell 社群活动详情。",
75+
description: ensureSeoDescription({
76+
description: data.event.description,
77+
title: data.event.title,
78+
sectionPath: ["events"],
79+
locale: "zh",
80+
}),
6481
};
6582
}
6683

app/[locale]/feed/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,20 @@ import { FeedAuthWrapper } from "@/app/[locale]/feed/components/FeedAuthWrapper"
1919
import type { SharedLinkView, CategorySlug } from "@/app/[locale]/feed/types";
2020
import type { ApiResponse } from "@/app/[locale]/feed/types";
2121
import Link from "next/link";
22+
import { ensureSeoDescription } from "@/lib/seo-description";
2223

2324
export const revalidate = 120;
2425

26+
// 原 description 只有 24 字符(远低于 Bing 推荐的 150-160),统一走 ensureSeoDescription
27+
// 兜底到 80+ 字符。社区分享墙是公开 SEO 页,搜索摘要质量直接影响 CTR。
2528
export const metadata: Metadata = {
2629
title: "社区分享墙 · Involution Hell",
27-
description: "群友精选好文,随手转发,沉淀有价值的信息流。",
30+
description: ensureSeoDescription({
31+
description: "群友精选好文,随手转发,沉淀有价值的信息流。",
32+
title: "社区分享墙",
33+
sectionPath: ["feed"],
34+
locale: "zh",
35+
}),
2836
};
2937

3038
/**

content/docs/career/events/coffee-chat.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Senior Tech-Industry Engineer Coffee Chat Recap
3-
description: ""
3+
description: "Senior backend engineer shares Australian programmer job search strategies, big company career planning, and platform resources for early-career tech professionals."
44
date: "2025-11-01"
55
tags:
66
- career

content/docs/career/events/coffee-chat.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: 资深科技大厂程序员Coffee Chat回顾
3-
description: ""
3+
description: "资深科技大厂程序员Coffee Chat回顾:澳洲程序员求职与职业规划分享会,聚焦大公司作为职业跳板的核心策略与SRE入门路径,详解K8s自动化项目优先于安全背景的实战建议。适合CS/AI求职者、海外程序员及准备回国发展的技术从业者阅读。"
44
date: "2025-11-01"
55
tags:
66
- career

content/docs/career/events/event-takeway.en.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
title: Career Events Recap Hub
3-
description: Career Events Recap Introduction Page
3+
description: "Explore career event takeaways on involutionhell.com: interview tips, resume optimization, and role-specific growth insights for job seekers."
44
date: "2025-10-26"
55
tags:
66
- intro

0 commit comments

Comments
 (0)