feat(docs): 文档页底部显示最近 5 次更新(GitHub API)#279
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a “recent updates” panel to documentation pages by introducing a server-side API route that queries GitHub commit history for a doc file, then rendering the latest 5 commits in a client component.
Changes:
- Add
/api/docs/historyroute that fetches recent commits for a given file path via GitHub REST API, with caching headers + revalidate. - Add
DocHistoryPanelclient component to display recent commits (avatar/author/relative time/message). - Mount the panel on doc pages between Giscus comments and the license notice.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| app/docs/[...slug]/page.tsx | Mounts DocHistoryPanel on the docs page and passes the doc file path. |
| app/components/DocHistoryPanel.tsx | New client UI to fetch and render the latest commit history list. |
| app/api/docs/history/route.ts | New API endpoint that queries GitHub commits by path and returns normalized history items with caching. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { searchParams } = new URL(req.url); | ||
| const path = searchParams.get("path"); | ||
|
|
||
| if (!path) { | ||
| return NextResponse.json( | ||
| { success: false, error: "缺少 path 参数" }, | ||
| { status: 400 }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
path 直接来自 query string,目前只做了“是否为空”的校验就用于拼接 GitHub API 请求。建议至少限制为文档目录内的相对路径(例如只允许以 app/docs/ 或 docsBase 开头,拒绝包含 ..、反斜杠等),避免用服务端的 GitHub token 被动暴露任意仓库文件的 commit 信息。
| const apiUrl = `https://api.github.com/repos/InvolutionHell/involutionhell/commits?path=${encodeURIComponent(path)}&per_page=5`; | ||
|
|
There was a problem hiding this comment.
这里把客户端传来的 path 直接传给 commits?path=...,但页面侧传入的是 page.file.path(在 getDocContributorsByPath 里会再统一前缀成 app/docs/...)。如果 path 是 docs 相对路径(不含 app/docs/),GitHub 会查不到文件导致结果为空。建议在服务端统一把 path 规范化成 repo-root 相对路径(复用 lib/github 的 docsBase/normalizeDocsPath),并避免在此处硬编码 owner/repo。
| if (res.status === 403) { | ||
| return NextResponse.json( | ||
| { success: false, error: "GitHub API 限流,请稍后重试" }, | ||
| { status: 429 }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
将 GitHub 的所有 403 都映射为 429(限流)不准确:403 也可能是 token 权限不足/仓库不可访问等。建议读取响应体 message 或根据 x-ratelimit-remaining/x-ratelimit-reset 判断是否真的触发 rate limit;否则返回更贴近真实原因的 502/401/403 错误。
app/api/docs/history/route.ts
Outdated
| avatarUrl: | ||
| c.author?.avatar_url ?? `https://github.com/${c.commit.author.name}.png`, |
There was a problem hiding this comment.
当 c.author 为 null 时,这里用 c.commit.author.name 拼 https://github.com/<name>.png 作为头像兜底;但 commit.author.name 通常是展示名而不是 GitHub login(可能包含空格/中文),很容易 404。建议在 author 缺失时返回空头像/占位图,或把 avatarUrl 设为固定的默认头像资源,并把 authorLogin 明确为展示名。
| avatarUrl: | |
| c.author?.avatar_url ?? `https://github.com/${c.commit.author.name}.png`, | |
| avatarUrl: c.author?.avatar_url ?? "", |
app/components/DocHistoryPanel.tsx
Outdated
| import type { HistoryItem } from "@/app/api/docs/history/route"; | ||
|
|
There was a problem hiding this comment.
DocHistoryPanel 是 client component,但这里从 app/api/.../route 里直接导入类型会把 UI 层和 Route Handler 文件耦合在一起(后续 route 文件引入 node-only 依赖时更容易踩到边界问题)。建议把 HistoryItem 提到独立的共享类型模块(例如 app/types/docs-history.ts / lib/types),client 与 server 都从该模块引用。
| import type { HistoryItem } from "@/app/api/docs/history/route"; | |
| interface HistoryItem { | |
| userName: string; | |
| userImage?: string | null; | |
| timestamp: string; | |
| } |
app/components/DocHistoryPanel.tsx
Outdated
| fetch(`/api/docs/history?path=${encodeURIComponent(path)}`) | ||
| .then((r) => r.json()) | ||
| .then((json) => { | ||
| if (cancelled) return; | ||
| if (json.success) { | ||
| setItems(json.data); | ||
| } else { | ||
| setError(json.error ?? "无法加载历史"); | ||
| } | ||
| }) | ||
| .catch(() => { | ||
| if (!cancelled) setError("无法加载历史"); |
There was a problem hiding this comment.
当前 error 和 items 的渲染条件是独立的:当 path 变化触发重新 fetch 时,如果请求失败会 setError(...) 但不会清空旧的 items,从而出现“错误提示 + 旧历史列表”同时显示的情况。建议在 effect 开始时先重置 setItems(null); setError(null);,并在渲染历史列表时额外要求 error === null(或在出错时清空 items)。
| fetch(`/api/docs/history?path=${encodeURIComponent(path)}`) | |
| .then((r) => r.json()) | |
| .then((json) => { | |
| if (cancelled) return; | |
| if (json.success) { | |
| setItems(json.data); | |
| } else { | |
| setError(json.error ?? "无法加载历史"); | |
| } | |
| }) | |
| .catch(() => { | |
| if (!cancelled) setError("无法加载历史"); | |
| setItems(null); | |
| setError(null); | |
| fetch(`/api/docs/history?path=${encodeURIComponent(path)}`) | |
| .then((r) => r.json()) | |
| .then((json) => { | |
| if (cancelled) return; | |
| if (json.success) { | |
| setItems(json.data); | |
| setError(null); | |
| } else { | |
| setItems(null); | |
| setError(json.error ?? "无法加载历史"); | |
| } | |
| }) | |
| .catch(() => { | |
| if (!cancelled) { | |
| setItems(null); | |
| setError("无法加载历史"); | |
| } |
Copilot CR #279: - route: 新增 normalizeDocsPath 做路径校验,只允许 app/docs/ 下相对路径, 拒绝 ..、反斜杠、null 字节,消除 SSRF 风险 - route: 接受 'docs/...' 和 '/docs/...' 形式,统一补成仓库根相对 'app/docs/...' - route: 403 用 x-ratelimit-remaining 区分限流 vs 权限不足,401 单独处理 - route: author 为 null 时 avatarUrl 返回空串而不是拼 github.com/<name>.png 容易 404 - 类型 HistoryItem 抽到 app/types/docs-history.ts,解耦 client 组件与 route handler - DocHistoryPanel: path 变化先清空 items/error 避免 '错误 + 旧列表' 同时显示 - DocHistoryPanel: 空头像用 data URI 占位防 Image 报错
接着前一 commit 补落下的 route.ts 改动(SSRF 防护 + 403 区分限流/权限不足 + 401 单独处理 + 头像兜底)
…int 错误 PR #279 build 挂在 lint: > 50 | setItems(null); | ^ Avoid calling setState() directly within an effect 把 items / error / loading 合并成 discriminated union + useReducer, effect 里只 dispatch 一次,规避 lint 规则。 副收益:三种状态天然互斥,不会出现'错误提示 + 旧列表'并存的情况 (这正是上个 CR 试图用两次 setState 解决的问题,现在用状态机更干净)。
Summary
Test plan