Skip to content

Commit b1efac5

Browse files
committed
feat: 已将贡献者数据改为由后端脚本落盘的 JSON,并在文档页直接读取渲染。
1 parent 2e85820 commit b1efac5

File tree

7 files changed

+299
-112
lines changed

7 files changed

+299
-112
lines changed

.github/workflows/sync-uuid.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ on:
1212
- "package.json"
1313
- "pnpm-lock.yaml"
1414
- ".github/workflows/sync-uuid.yml"
15+
- "generated/doc-contributors.json"
1516
workflow_dispatch: {}
1617

1718
concurrency:
@@ -56,8 +57,8 @@ jobs:
5657
- name: Auto-commit added docIds (if any)
5758
uses: stefanzweifel/git-auto-commit-action@v5
5859
with:
59-
commit_message: "chore(docs): add missing docId via uuid.mjs [skip ci]" # ← 防循环
60-
file_pattern: "app/docs/**/*.md app/docs/**/*.mdx app/docs/**/*.markdown"
60+
commit_message: "chore(docs): sync doc metadata [skip ci]" # ← 防循环
61+
file_pattern: "app/docs/**/*.md app/docs/**/*.mdx app/docs/**/*.markdown generated/doc-contributors.json"
6162

6263
- name: Backfill contributors & sync DB
6364
run: pnpm exec node scripts/backfill-contributors.mjs
@@ -66,5 +67,5 @@ jobs:
6667
uses: actions/upload-artifact@v4
6768
with:
6869
name: doc-contributors-snapshot
69-
path: tmp/doc-contributors.json
70+
path: generated/doc-contributors.json
7071
if-no-files-found: ignore

app/components/Contributors.tsx

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import Image from "next/image";
22
import Link from "next/link";
3+
import type { DocContributorsRecord } from "@/lib/contributors";
34

4-
interface Contributor {
5-
login: string;
6-
avatar_url: string;
7-
html_url: string;
5+
interface ContributorsProps {
6+
entry: DocContributorsRecord | null;
87
}
98

10-
export function Contributors({
11-
contributors,
12-
}: {
13-
contributors: Contributor[];
14-
}) {
9+
function formatLastContributedAt(value: string | null) {
10+
if (!value) return null;
11+
const date = new Date(value);
12+
if (Number.isNaN(date.getTime())) return null;
13+
return new Intl.DateTimeFormat("zh-CN", {
14+
year: "numeric",
15+
month: "2-digit",
16+
day: "2-digit",
17+
}).format(date);
18+
}
19+
20+
export function Contributors({ entry }: ContributorsProps) {
21+
const contributors = entry?.contributors ?? [];
22+
1523
if (contributors.length === 0) {
1624
return null;
1725
}
@@ -21,25 +29,54 @@ export function Contributors({
2129
<hr className="border-border/70 !mt-10 !mb-5" />
2230
<h2 id="contributors-heading">贡献者</h2>
2331
<ul className="mt-0 mb-0 flex flex-wrap items-center gap-x-6 gap-y-4 list-none p-0">
24-
{contributors.map((contributor) => (
25-
<li key={contributor.login}>
26-
<Link
27-
href={contributor.html_url}
28-
target="_blank"
29-
rel="noopener noreferrer"
30-
className="inline-flex items-center gap-3 text-base font-medium text-primary transition-colors hover:text-primary/80 no-underline"
31-
>
32+
{contributors.map((contributor) => {
33+
const displayName = contributor.login ?? `#${contributor.githubId}`;
34+
const href = contributor.htmlUrl ?? undefined;
35+
const avatarSrc =
36+
contributor.avatarUrl ??
37+
`https://avatars.githubusercontent.com/u/${contributor.githubId}`;
38+
const lastDate = formatLastContributedAt(
39+
contributor.lastContributedAt,
40+
);
41+
42+
const content = (
43+
<>
3244
<Image
33-
src={contributor.avatar_url}
34-
alt={contributor.login}
45+
src={avatarSrc}
46+
alt={displayName}
3547
width={35}
3648
height={35}
3749
className="!m-0 h-10 w-10 rounded-full border border-border/50 object-cover shadow-sm"
3850
/>
39-
<span>{contributor.login}</span>
40-
</Link>
41-
</li>
42-
))}
51+
<span className="flex flex-col text-left leading-tight">
52+
<span className="font-medium">{displayName}</span>
53+
<span className="text-sm text-muted-foreground">
54+
贡献 {contributor.contributions}
55+
{lastDate ? ` · 最近 ${lastDate}` : ""}
56+
</span>
57+
</span>
58+
</>
59+
);
60+
61+
return (
62+
<li key={contributor.githubId}>
63+
{href ? (
64+
<Link
65+
href={href}
66+
target="_blank"
67+
rel="noopener noreferrer"
68+
className="inline-flex items-center gap-3 text-base text-primary transition-colors hover:text-primary/80 no-underline"
69+
>
70+
{content}
71+
</Link>
72+
) : (
73+
<div className="inline-flex items-center gap-3 text-base">
74+
{content}
75+
</div>
76+
)}
77+
</li>
78+
);
79+
})}
4380
</ul>
4481
<hr className="!mb-0 !mt-5 border-border/70" />
4582
</section>

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

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type { Metadata } from "next";
55
import { getMDXComponents } from "@/mdx-components";
66
import { GiscusComments } from "@/app/components/GiscusComments";
77
import { EditOnGithub } from "@/app/components/EditOnGithub";
8-
import { buildDocsEditUrl, getContributors } from "@/lib/github";
8+
import { buildDocsEditUrl } from "@/lib/github";
9+
import {
10+
getDocContributorsByPath,
11+
getDocContributorsByDocId,
12+
} from "@/lib/contributors";
913
import { Contributors } from "@/app/components/Contributors";
1014
import { DocsAssistant } from "@/app/components/DocsAssistant";
1115
import fs from "fs/promises";
@@ -44,10 +48,15 @@ export default async function DocPage({ params }: Param) {
4448

4549
// 统一通过工具函数生成 Edit 链接,内部已处理中文目录编码
4650
const editUrl = buildDocsEditUrl(page.path);
47-
// Get file path for contributors
48-
const filePath = "app/docs/" + page.file.path;
49-
// Fetch contributors data on server side
50-
const contributors = await getContributors(filePath);
51+
const docIdFromPage =
52+
(page.data as { docId?: string; frontmatter?: { docId?: string } })
53+
?.docId ??
54+
(page.data as { docId?: string; frontmatter?: { docId?: string } })
55+
?.frontmatter?.docId;
56+
57+
const contributorsEntry =
58+
getDocContributorsByPath(page.file.path) ||
59+
getDocContributorsByDocId(docIdFromPage);
5160
const Mdx = page.data.body;
5261

5362
// Prepare page content for AI assistant
@@ -75,7 +84,7 @@ export default async function DocPage({ params }: Param) {
7584
<EditOnGithub href={editUrl} />
7685
</div>
7786
<Mdx components={getMDXComponents()} />
78-
<Contributors contributors={contributors} />
87+
<Contributors entry={contributorsEntry} />
7988
<section className="mt-16">
8089
<GiscusComments />
8190
</section>

generated/doc-contributors.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"repo": "",
3+
"generatedAt": "",
4+
"docsDir": "app/docs",
5+
"totalDocs": 0,
6+
"results": []
7+
}

lib/contributors.ts

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,60 @@
1-
/**
2-
* @description 贡献者相关工具方法
3-
* @author Siz Long
4-
* @date 2025-09-27
5-
*/
1+
import dataset from "@/generated/doc-contributors.json";
2+
3+
export interface ContributorEntry {
4+
githubId: string;
5+
contributions: number;
6+
lastContributedAt: string | null;
7+
login?: string | null;
8+
avatarUrl?: string | null;
9+
htmlUrl?: string | null;
10+
}
11+
12+
export interface DocContributorsRecord {
13+
docId: string;
14+
path: string | null;
15+
contributorStats: Record<string, number>;
16+
contributors: ContributorEntry[];
17+
}
18+
19+
export interface ContributorsDataset {
20+
repo: string;
21+
generatedAt: string;
22+
docsDir: string;
23+
totalDocs: number;
24+
results: DocContributorsRecord[];
25+
}
26+
27+
const contributorsDataset = dataset as ContributorsDataset;
28+
29+
function normalizeRelativePath(relativePath: string): string {
30+
const cleaned = relativePath.replace(/^\/+/, "").replace(/\\/g, "/");
31+
return `app/docs/${cleaned}`;
32+
}
33+
34+
export function getContributorsDataset(): ContributorsDataset {
35+
return contributorsDataset;
36+
}
37+
38+
export function getDocContributorsByPath(
39+
relativeDocPath: string,
40+
): DocContributorsRecord | null {
41+
if (!relativeDocPath) return null;
42+
const normalized = normalizeRelativePath(relativeDocPath);
43+
return (
44+
contributorsDataset.results.find((entry) => entry.path === normalized) ??
45+
null
46+
);
47+
}
48+
49+
export function getDocContributorsByDocId(
50+
docId: string | undefined | null,
51+
): DocContributorsRecord | null {
52+
if (!docId) return null;
53+
return (
54+
contributorsDataset.results.find((entry) => entry.docId === docId) ?? null
55+
);
56+
}
57+
58+
export function listDocContributors(): DocContributorsRecord[] {
59+
return contributorsDataset.results;
60+
}

lib/github.ts

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { cache } from "react";
2-
31
// GitHub 相关工具方法:集中维护仓库常量与文档路径生成逻辑
42
const GITHUB_OWNER = "InvolutionHell";
53
const GITHUB_REPO = "involutionhell.github.io";
@@ -55,61 +53,3 @@ export const githubConstants = {
5553
docsBase: DOCS_BASE,
5654
repoBaseUrl: REPO_BASE_URL,
5755
};
58-
59-
60-
61-
// Define contributor data structure
62-
interface Contributor {
63-
login: string;
64-
avatar_url: string;
65-
html_url: string;
66-
}
67-
68-
// Use React's cache function for request caching and deduplication
69-
export const getContributors = cache(
70-
async (filePath: string): Promise<Contributor[]> => {
71-
try {
72-
// Use GITHUB_TOKEN environment variable for authorization to increase API rate limit
73-
const headers: HeadersInit = {};
74-
if (process.env.GITHUB_TOKEN) {
75-
headers.Authorization = `token ${process.env.GITHUB_TOKEN}`;
76-
}
77-
78-
const response = await fetch(
79-
`https://api.github.com/repos/InvolutionHell/involutionhell.github.io/commits?path=${filePath}`,
80-
{
81-
headers,
82-
// Use next.revalidate to configure cache strategy (e.g., revalidate every hour)
83-
next: { revalidate: 3600 },
84-
},
85-
);
86-
87-
if (!response.ok) {
88-
// If request fails, return empty array
89-
console.error(
90-
`Failed to fetch contributors for ${filePath}: ${response.statusText}`,
91-
);
92-
return [];
93-
}
94-
95-
const commits = await response.json();
96-
97-
// Use Map to deduplicate contributors
98-
const uniqueContributors = new Map<string, Contributor>();
99-
commits.forEach((commit: { author?: Contributor }) => {
100-
if (commit.author) {
101-
uniqueContributors.set(commit.author.login, {
102-
login: commit.author.login,
103-
avatar_url: commit.author.avatar_url,
104-
html_url: commit.author.html_url,
105-
});
106-
}
107-
});
108-
109-
return Array.from(uniqueContributors.values());
110-
} catch (error) {
111-
console.error(`Error fetching contributors for ${filePath}:`, error);
112-
return [];
113-
}
114-
},
115-
);

0 commit comments

Comments
 (0)