From 847d2c67ce42ecf1accd16d0579e6a58a700d8f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:14:33 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(rank):=20=E6=9C=AA=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E8=B4=A1=E7=8C=AE=E8=80=85=E4=B8=8D=E5=86=8D=E4=BA=A7=E7=94=9F?= =?UTF-8?q?=20/u/=20=E6=AD=BB=E9=93=BE=E2=80=94=E2=80=94hasProfile=20?= =?UTF-8?q?=E6=89=93=E6=A0=87=20+=20=E5=85=9C=E5=BA=95=E6=A1=A3=E6=A1=88?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 方案 A:generate-leaderboard.mts 构建时逐人探测 profile 接口,JSON 加 hasProfile;ContributorRow 仅对已注册者渲染 VIEW DOSSIER;sitemap 不再 收录未注册者的 /u/{id}(此前 13 个死链是 sitemap 主动喂给爬虫的)。 方案 B:/u/{githubId} 后端查无此人时,若命中 leaderboard 则渲染"尚未 入驻"兜底页(仓库贡献统计 + GitHub 外链 + 登录认领 CTA,数据全部来自 build-time JSON,不打后端),不再掉进通用 404;非贡献者仍 404。 Closes #372 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RtSNVtrEkCsFVHrbt4Zjgi --- app/[locale]/u/[username]/page.tsx | 142 ++++++++++++++++++++++--- app/components/rank/ContributorRow.tsx | 22 ++-- app/sitemap.ts | 5 +- generated/site-leaderboard.json | 63 +++++++---- messages/en.json | 4 + messages/zh.json | 4 + scripts/generate-leaderboard.mts | 43 ++++++++ 7 files changed, 238 insertions(+), 45 deletions(-) diff --git a/app/[locale]/u/[username]/page.tsx b/app/[locale]/u/[username]/page.tsx index 38934ea9..ca812cbc 100644 --- a/app/[locale]/u/[username]/page.tsx +++ b/app/[locale]/u/[username]/page.tsx @@ -242,6 +242,29 @@ function sleep(ms: number): Promise { return new Promise((r) => setTimeout(r, ms)); } +type LeaderboardRow = { + id: string; + name: string; + points?: number; + commits?: number; + avatarUrl?: string; + contributedDocs?: Array<{ id: string; title: string; url: string }>; + dailyCounts?: Record; +}; +// 先经 unknown 再转 Row[]:JSON 的字面量类型(每条 dailyCounts 都是独立的 literal)和 Row 的 +// Record 索引签名不兼容,tsc --noEmit 会报 TS2352。先走 unknown 绕开。 +const leaderboardRows = leaderboard as unknown as LeaderboardRow[]; + +/** + * 兜底页(#372)用:URL 里的 identifier("150361711" 或 "github_150361711") + * → leaderboard 行。只有纯数字 githubId 才可能命中。 + */ +function findLeaderboardRow(identifier: string): LeaderboardRow | undefined { + const gid = identifier.replace(/^github_/, ""); + if (!/^\d+$/.test(gid)) return undefined; + return leaderboardRows.find((r) => r.id === gid); +} + /** * 从 leaderboard JSON 按 githubId 匹配贡献记录。 * 之前按 name 字符串匹配会踩坑(leaderboard.name = "longsizhuo",user_accounts.username = "github_114939201"), @@ -250,20 +273,8 @@ function sleep(ms: number): Promise { function findContributions(githubId: number | null | undefined) { if (githubId == null) return { docs: [], points: 0, commits: 0, dailyCounts: {} }; - type Row = { - id: string; - name: string; - points?: number; - commits?: number; - avatarUrl?: string; - contributedDocs?: Array<{ id: string; title: string; url: string }>; - dailyCounts?: Record; - }; - // 先经 unknown 再转 Row[]:JSON 的字面量类型(每条 dailyCounts 都是独立的 literal)和 Row 的 - // Record 索引签名不兼容,tsc --noEmit 会报 TS2352。先走 unknown 绕开。 - const rows = leaderboard as unknown as Row[]; const idStr = String(githubId); - const match = rows.find((r) => r.id === idStr); + const match = leaderboardRows.find((r) => r.id === idStr); if (!match) return { docs: [], points: 0, commits: 0, dailyCounts: {} }; return { docs: match.contributedDocs ?? [], @@ -273,6 +284,95 @@ function findContributions(githubId: number | null | undefined) { }; } +/** + * 未注册贡献者的兜底页(#372):git 有贡献但没登录过本站的人,点进 /u/{id} + * 以前会掉进通用 404,现在展示仓库贡献统计 + GitHub 外链 + 登录认领 CTA。 + * 数据全部来自 build-time 的 leaderboard JSON,不打后端。 + */ +async function UnregisteredContributor({ row }: { row: LeaderboardRow }) { + const t = await getTranslations("profile"); + const docs = row.contributedDocs ?? []; + return ( + <> +
+
+
+
+
+ {t("dossier")} +
+

+ {row.name} +

+
+ +
+ {row.avatarUrl && ( +
+ {row.name} +
+ )} +
+

{t("unregistered.note")}

+
+ + {row.points ?? 0} {t("stats.points")} + + + {row.commits ?? 0} {t("stats.commits")} + +
+
+ + GITHUB → + + + {t("unregistered.cta")} + +
+
+
+ + {docs.length > 0 && ( +
+

+ {t("docs.heading")} +

+
    + {docs.map((doc) => ( +
  • + + {doc.title} + +
  • + ))} +
+
+ )} +
+
+