Skip to content

Commit 94d8de4

Browse files
committed
fix(pr281): 修复 Copilot Review 提出的 6 处问题
1. top-docs/route.ts: limit 参数加 Number.isFinite 校验防 NaN 2. top-docs/route.ts: 返回结构统一为 ApiResponse{success,data},补齐 title 字段 (通过 fumadocs source.getPage 回填,同时保留埋点带来的 title) 3. HotDocsPreview.tsx: 改走同源 /api/analytics/top-docs 走 ISR, 不再直连 BACKEND_URL,消除 '白做缓存' 问题 4. docs/[...slug]/page.tsx: generateMetadata 也按 locale 取页面, 避免英文页显示中文 title/description 5. middleware.ts: Accept-Language 解析改为按 q 值排序取首选语言, 正确处理 'fr-CA,fr;q=0.9,en;q=0.8' 这类多语言 header 6. computer-science/index.en.mdx: 删除正文末尾混入的整段中文
1 parent c001f95 commit 94d8de4

6 files changed

Lines changed: 105 additions & 77 deletions

File tree

app/api/analytics/top-docs/route.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
11
import { prisma } from "@/lib/db";
22
import { NextRequest } from "next/server";
3+
import { source } from "@/lib/source";
34

45
export const revalidate = 300;
56

7+
/** 将 NaN/非正数的 limit 回退到默认值,同时加上限保护 */
8+
function parseLimit(raw: string | null, fallback = 5, max = 20): number {
9+
const n = Number(raw);
10+
if (!Number.isFinite(n) || n <= 0) return fallback;
11+
return Math.min(Math.floor(n), max);
12+
}
13+
14+
/**
15+
* 从 path 尝试解析为文档标题:/docs/ai/rl → 查 fumadocs source
16+
* 查不到时回退为 path 最后一段。
17+
*/
18+
function resolveTitle(path: string): string {
19+
// /docs/ai/rl → ["ai", "rl"]
20+
const slug = path
21+
.replace(/^\/docs\/?/, "")
22+
.split("/")
23+
.filter(Boolean);
24+
if (slug.length === 0) return path;
25+
const page = source.getPage(slug);
26+
if (page?.data?.title) return page.data.title as string;
27+
return slug[slug.length - 1];
28+
}
29+
630
export async function GET(req: NextRequest) {
731
const { searchParams } = new URL(req.url);
832
const window = searchParams.get("window") ?? "7d";
9-
const limit = Math.min(Number(searchParams.get("limit") ?? "5"), 20);
33+
const limit = parseLimit(searchParams.get("limit"));
1034

1135
const since = new Date();
1236
if (window === "7d") {
@@ -28,19 +52,27 @@ export async function GET(req: NextRequest) {
2852
});
2953

3054
// 统计各路径 PV(内存过滤 /docs/ 前缀)
31-
const counts: Record<string, number> = {};
55+
const counts: Record<string, { count: number; title?: string }> = {};
3256
for (const row of rows) {
3357
const data = row.eventData as { path?: string; title?: string } | null;
3458
const path = data?.path;
3559
if (path && path.startsWith("/docs/")) {
36-
counts[path] = (counts[path] ?? 0) + 1;
60+
if (!counts[path]) counts[path] = { count: 0, title: data?.title };
61+
counts[path].count += 1;
62+
// 优先保留带 title 的埋点数据
63+
if (!counts[path].title && data?.title) counts[path].title = data.title;
3764
}
3865
}
3966

4067
const top = Object.entries(counts)
41-
.sort((a, b) => b[1] - a[1])
68+
.sort((a, b) => b[1].count - a[1].count)
4269
.slice(0, limit)
43-
.map(([path, views]) => ({ path, views }));
70+
.map(([path, { count, title }]) => ({
71+
path,
72+
title: title ?? resolveTitle(path),
73+
views: count,
74+
}));
4475

45-
return Response.json(top);
76+
// 统一 ApiResponse 包裹,和后端 /analytics/top-docs 以及 /rank HotDocsTab 一致
77+
return Response.json({ success: true, data: top });
4678
}

app/components/HotDocsPreview.tsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,25 @@ interface TopDocDto {
77
}
88

99
async function fetchTopDocs(): Promise<TopDocDto[]> {
10-
const backendUrl = process.env.BACKEND_URL ?? "http://localhost:8081";
10+
// 同源请求 Next ISR 路由,避开对 BACKEND_URL 的硬依赖,
11+
// 并复用 app/api/analytics/top-docs 的 revalidate=300 缓存。
12+
// Server Component 中 fetch 需要绝对 URL,优先读显式站点地址,
13+
// 其次 VERCEL_URL(预览/生产),本地回退到 3010。
14+
const siteUrl =
15+
process.env.NEXT_PUBLIC_SITE_URL ??
16+
(process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : null) ??
17+
"http://localhost:3010";
1118
try {
1219
const res = await fetch(
13-
`${backendUrl}/analytics/top-docs?window=7d&limit=5`,
14-
{ next: { revalidate: 300 } },
20+
`${siteUrl}/api/analytics/top-docs?window=7d&limit=5`,
21+
{
22+
next: { revalidate: 300 },
23+
},
1524
);
1625
if (!res.ok) return [];
1726
const json = await res.json();
18-
// 后端用 ApiResponse<List<TopDocDto>> 包裹,data 字段存实际数据
19-
return json.data ?? json;
27+
// 统一 ApiResponse<{ path, title, views }[]> 结构
28+
return Array.isArray(json?.data) ? json.data : [];
2029
} catch {
2130
return [];
2231
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ export async function generateStaticParams() {
137137

138138
export async function generateMetadata({ params }: Param): Promise<Metadata> {
139139
const { slug } = await params;
140-
const page = source.getPage(slug);
140+
const locale = await getLocaleFromCookie();
141+
// metadata 需与页面主体同语言,避免英文页显示中文 title/desc 造成 SEO 错乱
142+
const { page } = getPageWithLocale(slug, locale);
141143
if (page == null) {
142144
notFound();
143145
}

app/docs/computer-science/index.en.mdx

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,34 +42,3 @@ We recommend learning in the following order:
4242
---
4343

4444
_This knowledge base is maintained by a student community. Contributions are welcome!_
45-
46-
# 计算机科学
47-
48-
欢迎来到计算机科学知识库!我们在此收集了计算机科学各个领域的核心概念和深入分析。
49-
50-
## 主体内容
51-
52-
### 数据结构和算法
53-
54-
- [数据结构基础](/computer-science/data-structures)
55-
- 常见算法分析
56-
- 复杂性理论
57-
58-
### 编程语言
59-
60-
- 编程范式
61-
- 语言设计原则
62-
- 编译器理论
63-
64-
## 学习建议
65-
66-
我们建议按照以下顺序学习:
67-
68-
1. 首先掌握基本的数据结构
69-
2. 理解常见算法的实现
70-
3. 学习算法复杂度分析
71-
4. 深入特定领域的高级主题
72-
73-
---
74-
75-
_This knowledge base is maintained by a student community. Contributions are welcome!_

generated/site-leaderboard.json

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -28,33 +28,33 @@
2828
},
2929
{
3030
"id": "gmpls10e2dz0bbizotvhglc8",
31-
"title": "Static Array",
32-
"url": "/docs/computer-science/data-structures/array/01-static-array"
31+
"title": "静态数组",
32+
"url": "/docs/computer-science/data-structures/array/01-static-array.zh"
3333
},
3434
{
3535
"id": "nuojcaq1s6r5nggul0uq3r3j",
36-
"title": "Dynamic Array",
37-
"url": "/docs/computer-science/data-structures/array/02-dynamic-array"
36+
"title": "动态数组",
37+
"url": "/docs/computer-science/data-structures/array/02-dynamic-array.zh"
3838
},
3939
{
4040
"id": "ai7cmwf4irjaobqf7uokj3b4",
41-
"title": "Array",
42-
"url": "/docs/computer-science/data-structures/array"
41+
"title": "数组",
42+
"url": "/docs/computer-science/data-structures/array/index.zh"
4343
},
4444
{
4545
"id": "vti0bt2qlnr681msbk6igznc",
46-
"title": "Data Structures Fundamentals",
47-
"url": "/docs/computer-science/data-structures"
46+
"title": "数据结构基础",
47+
"url": "/docs/computer-science/data-structures/index.zh"
4848
},
4949
{
5050
"id": "gkjk6stzpb44n9lv8u2ij7xx",
51-
"title": "Singly Linked List",
52-
"url": "/docs/computer-science/data-structures/linked-list/01-singly-linked-list"
51+
"title": "单链表",
52+
"url": "/docs/computer-science/data-structures/linked-list/01-singly-linked-list.zh"
5353
},
5454
{
5555
"id": "lt9yrqt0ksl2liabq9ocw0z4",
56-
"title": "Linked List",
57-
"url": "/docs/computer-science/data-structures/linked-list"
56+
"title": "链表",
57+
"url": "/docs/computer-science/data-structures/linked-list/index.zh"
5858
},
5959
{
6060
"id": "i88bna4sg5pr4ekhg32drv2i",
@@ -549,7 +549,7 @@
549549
{
550550
"id": "l6eepr5ctjgrhdgupy3twr1t",
551551
"title": "Prompt Repetition Improves Non-Reasoning LLMs",
552-
"url": "/docs/CommunityShare/Amazing-AI-Tools/prompt-repetition-improves-non-reasoning-llms"
552+
"url": "/docs/CommunityShare/Amazing-AI-Tools/prompt-repetition-improves-non-reasoning-llms.zh"
553553
},
554554
{
555555
"id": "rv6egbynttb4mt1n0412bue0",
@@ -658,8 +658,8 @@
658658
},
659659
{
660660
"id": "fostlzqqx6l10qz1egd8dw5m",
661-
"title": "Counting Stars-Inter-Uni Programming Contest.md",
662-
"url": "/docs/CommunityShare/Leetcode/Counting Stars-Inter-Uni Programming Contest"
661+
"title": "Counting Stars — 校际编程竞赛",
662+
"url": "/docs/CommunityShare/Leetcode/counting-stars-inter-uni-programming-contest.zh"
663663
},
664664
{
665665
"id": "l4db26ijmpeivh78a21981ia",
@@ -1168,7 +1168,7 @@
11681168
{
11691169
"id": "l6eepr5ctjgrhdgupy3twr1t",
11701170
"title": "Prompt Repetition Improves Non-Reasoning LLMs",
1171-
"url": "/docs/CommunityShare/Amazing-AI-Tools/prompt-repetition-improves-non-reasoning-llms"
1171+
"url": "/docs/CommunityShare/Amazing-AI-Tools/prompt-repetition-improves-non-reasoning-llms.zh"
11721172
}
11731173
]
11741174
},
@@ -1181,33 +1181,33 @@
11811181
"contributedDocs": [
11821182
{
11831183
"id": "gmpls10e2dz0bbizotvhglc8",
1184-
"title": "Static Array",
1185-
"url": "/docs/computer-science/data-structures/array/01-static-array"
1184+
"title": "静态数组",
1185+
"url": "/docs/computer-science/data-structures/array/01-static-array.zh"
11861186
},
11871187
{
11881188
"id": "nuojcaq1s6r5nggul0uq3r3j",
1189-
"title": "Dynamic Array",
1190-
"url": "/docs/computer-science/data-structures/array/02-dynamic-array"
1189+
"title": "动态数组",
1190+
"url": "/docs/computer-science/data-structures/array/02-dynamic-array.zh"
11911191
},
11921192
{
11931193
"id": "ai7cmwf4irjaobqf7uokj3b4",
1194-
"title": "Array",
1195-
"url": "/docs/computer-science/data-structures/array"
1194+
"title": "数组",
1195+
"url": "/docs/computer-science/data-structures/array/index.zh"
11961196
},
11971197
{
11981198
"id": "vti0bt2qlnr681msbk6igznc",
1199-
"title": "Data Structures Fundamentals",
1200-
"url": "/docs/computer-science/data-structures"
1199+
"title": "数据结构基础",
1200+
"url": "/docs/computer-science/data-structures/index.zh"
12011201
},
12021202
{
12031203
"id": "gkjk6stzpb44n9lv8u2ij7xx",
1204-
"title": "Singly Linked List",
1205-
"url": "/docs/computer-science/data-structures/linked-list/01-singly-linked-list"
1204+
"title": "单链表",
1205+
"url": "/docs/computer-science/data-structures/linked-list/01-singly-linked-list.zh"
12061206
},
12071207
{
12081208
"id": "lt9yrqt0ksl2liabq9ocw0z4",
1209-
"title": "Linked List",
1210-
"url": "/docs/computer-science/data-structures/linked-list"
1209+
"title": "链表",
1210+
"url": "/docs/computer-science/data-structures/linked-list/index.zh"
12111211
},
12121212
{
12131213
"id": "ksjj9shalh6hqezx6t6am5vw",
@@ -1631,7 +1631,7 @@
16311631
{
16321632
"id": "l6eepr5ctjgrhdgupy3twr1t",
16331633
"title": "Prompt Repetition Improves Non-Reasoning LLMs",
1634-
"url": "/docs/CommunityShare/Amazing-AI-Tools/prompt-repetition-improves-non-reasoning-llms"
1634+
"url": "/docs/CommunityShare/Amazing-AI-Tools/prompt-repetition-improves-non-reasoning-llms.zh"
16351635
}
16361636
]
16371637
},
@@ -1645,7 +1645,7 @@
16451645
{
16461646
"id": "l6eepr5ctjgrhdgupy3twr1t",
16471647
"title": "Prompt Repetition Improves Non-Reasoning LLMs",
1648-
"url": "/docs/CommunityShare/Amazing-AI-Tools/prompt-repetition-improves-non-reasoning-llms"
1648+
"url": "/docs/CommunityShare/Amazing-AI-Tools/prompt-repetition-improves-non-reasoning-llms.zh"
16491649
}
16501650
]
16511651
},

middleware.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,27 @@ export function middleware(req: NextRequest) {
2121
(req as NextRequest & { geo?: { country?: string } }).geo?.country ?? "";
2222
const acceptLang = req.headers.get("accept-language") ?? "";
2323

24-
// 默认中文;只有明确英文 Accept-Language 且非中国 IP 才切 en
24+
// 解析 Accept-Language header 按 q 值排序的优先级列表
25+
// 例如 "fr-CA,fr;q=0.9,en;q=0.8,zh;q=0.5" → [fr-CA, fr, en, zh]
26+
// 之前只 startsWith 判断会忽略 q 值较低但明确列出的语言。
27+
const preferred = acceptLang
28+
.split(",")
29+
.map((part) => {
30+
const [tag, ...params] = part.trim().split(";");
31+
const qParam = params.find((p) => p.trim().startsWith("q="));
32+
const q = qParam ? parseFloat(qParam.slice(2)) : 1;
33+
return { tag: tag.toLowerCase(), q: Number.isFinite(q) ? q : 0 };
34+
})
35+
.filter((item) => item.tag)
36+
.sort((a, b) => b.q - a.q);
37+
38+
const firstMatch = preferred.find((item) =>
39+
/^(en|zh)(-|$)/.test(item.tag),
40+
)?.tag;
41+
42+
// 默认中文;只有 Accept-Language 首选为英文且非中国 IP 才切 en
2543
const isExplicitlyEnglish =
26-
!acceptLang.toLowerCase().startsWith("zh") &&
27-
acceptLang.toLowerCase().startsWith("en") &&
28-
country !== "CN";
44+
firstMatch?.startsWith("en") === true && country !== "CN";
2945
const locale = isExplicitlyEnglish ? "en" : "zh";
3046

3147
const res = NextResponse.next();

0 commit comments

Comments
 (0)