为Firefly博客加入「更新日志」页面,记录并展示每一次的Git提交修改
前言
闲的没事用AI搓出来的,有BUG可以在评论指出
功能说明
新增一个 /changelog/ 页面,自动读取 git 提交历史并渲染成更新日志:
- 按日期倒序分组
- 提交按 Conventional Commits 前缀自动分类并上色(后文有详细说明)
- 默认展开最近 10 天,更早的自动折叠为
<details>可展开 - 每次
pnpm build自动刷新数据
涉及文件
| 文件 | 说明 |
|---|---|
scripts/generate-changelog.mjs | 生成数据脚本(读 git log → 写 src/constants/changelog.json) |
src/pages/changelog.astro | 更新日志页面本体 |
src/constants/changelog.json | 数据文件(脚本自动生成) |
src/config/siteConfig.ts | 加页面开关 |
src/types/siteConfig.ts | 加类型定义 |
src/config/navBarConfig.ts | 导航栏加入口 |
package.json | build 脚本开头接入生成步骤 |
添加步骤
放入生成脚本
在你的博客/scripts中新建文件generate-changelog.mjs并输入下列内容
/** * 更新日志数据生成脚本 * 读取 git 提交历史,按日期倒序分组,输出到 src/constants/changelog.json * 页面 src/pages/changelog.astro 读取渲染 * * 用法:node scripts/generate-changelog.mjs */import { spawnSync } from "node:child_process";import { writeFileSync, mkdirSync } from "node:fs";import { join, dirname } from "node:path";import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));const ROOT_DIR = join(__dirname, "..");const OUTPUT_FILE = join(ROOT_DIR, "src", "constants", "changelog.json");
// 提交类型 → 中文标签(Conventional Commits 前缀)const TYPE_LABELS = { feat: "新功能", fix: "修复", perf: "优化", docs: "文档", refactor: "重构", style: "样式", test: "测试", build: "构建", ci: "CI", chore: "杂项", revert: "回退", merge: "合并", update: "更新", improve: "改进",};
function detectType(subject) { const m = subject.match(/^([a-z]+)(\([^)]*\))?[::]/); if (m && TYPE_LABELS[m[1]]) return m[1]; return "other";}
// 拉深历史(应对 CF Pages / CI 浅克隆 depth=1),失败忽略spawnSync("git", ["fetch", "--unshallow"], { cwd: ROOT_DIR, stdio: "ignore",});
console.log("读取 git 提交历史...");const r = spawnSync("git", ["log", "--pretty=format:%h|%aI|%an|%s"], { cwd: ROOT_DIR, encoding: "utf8", maxBuffer: 64 * 1024 * 1024,});
// 容错:git 不可用(浅克隆 / 无 git 环境 / CI 限制)时生成空数据,不中断构建if (r.status !== 0) { console.warn( "[changelog] 警告: git log 执行失败(可能是浅克隆或无 git 环境),生成空数据。", ); const empty = { generatedAt: new Date().toISOString(), total: 0, groups: [], }; mkdirSync(dirname(OUTPUT_FILE), { recursive: true }); writeFileSync(OUTPUT_FILE, JSON.stringify(empty, null, 2)); console.log(`✅ 已生成空数据 ${OUTPUT_FILE}(total: 0)`); process.exit(0);}const log = r.stdout;
const groups = {};const lines = log.split("\n").filter(Boolean);for (const line of lines) { const [hash, isoDate, author, subject] = line.split("|"); if (!hash || !isoDate) continue; const date = isoDate.slice(0, 10); const type = detectType(subject); (groups[date] ??= []).push({ hash, date, time: isoDate.slice(11, 19), author, subject, type, });}
const data = { generatedAt: new Date().toISOString(), total: lines.length, groups: Object.entries(groups) .map(([date, commits]) => ({ date, commits })) .sort((a, b) => b.date.localeCompare(a.date)),};
mkdirSync(dirname(OUTPUT_FILE), { recursive: true });writeFileSync(OUTPUT_FILE, JSON.stringify(data, null, 2));console.log(`✅ 已生成 ${OUTPUT_FILE}`);console.log(` 共 ${data.total} 次提交,${data.groups.length} 个日期分组`);添加页面
在你的博客/src/pages中新建changelog.astro并输入下列内容:
---import { siteConfig } from "@/config/siteConfig";import MainGridLayout from "@/layouts/MainGridLayout.astro";import changelog from "@/constants/changelog.json";
// 检查页面是否启用if (!siteConfig.pages.changelog) { return Astro.redirect("/404/");}
interface Commit { hash: string; date: string; time: string; author: string; subject: string; type: string;}
// 提交类型 → 中文标签 + 语义色(与 scripts/generate-changelog.mjs 一致)const TYPE_META: Record<string, { label: string; color: string }> = { feat: { label: "新功能", color: "#22c55e" }, fix: { label: "修复", color: "#ef4444" }, perf: { label: "优化", color: "#3b82f6" }, docs: { label: "文档", color: "#8b5cf6" }, refactor: { label: "重构", color: "#f59e0b" }, style: { label: "样式", color: "#14b8a6" }, test: { label: "测试", color: "#a855f7" }, build: { label: "构建", color: "#64748b" }, ci: { label: "CI", color: "#64748b" }, chore: { label: "杂项", color: "#64748b" }, revert: { label: "回退", color: "#ef4444" }, merge: { label: "合并", color: "#06b6d4" }, update: { label: "更新", color: "#0ea5e9" }, improve: { label: "改进", color: "#10b981" }, other: { label: "提交", color: "#94a3b8" },};
const groups = changelog.groups as { date: string; commits: Commit[] }[];const EXPANDED_COUNT = 10; // 默认展开最近 10 组const generatedDate = changelog.generatedAt ? new Date(changelog.generatedAt).toLocaleDateString("zh-CN") : "";---
<MainGridLayout title="更新日志"> <div class="flex w-full rounded-(--radius-large) overflow-hidden relative min-h-32" > <div class="card-base z-10 px-9 py-6 relative w-full"> <!-- 页面标题 --> <div class="flex flex-col items-start justify-center mb-8"> <h1 class="text-4xl font-bold text-black/90 dark:text-white/90 mb-2 relative before:w-1 before:h-8 before:rounded-md before:bg-(--primary) before:absolute before:top-1/2 before:-translate-y-1/2 before:-left-4" > 更新日志 </h1> <p class="text-sm text-black/60 dark:text-white/60"> 共 {changelog.total} 次提交 · {groups.length} 个日期 {generatedDate && <>· 数据更新于 {generatedDate}</>} </p> </div>
<!-- 提交列表 --> <div class="flex flex-col gap-4"> { groups.map((group, gi) => ( group.commits.length > 0 && ( gi < EXPANDED_COUNT ? ( // 展开的分组 <div class="changelog-group"> <div class="changelog-date-row"> <span class="changelog-date-badge">{group.date}</span> <span class="changelog-date-count">{group.commits.length} 条提交</span> </div> <div class="changelog-list"> { group.commits.map((c) => ( <div class="changelog-item"> <span class="changelog-type" style={`color: ${TYPE_META[c.type]?.color ?? "#94a3b8"}; background: color-mix(in srgb, ${TYPE_META[c.type]?.color ?? "#94a3b8"} 12%, transparent);`} > {TYPE_META[c.type]?.label ?? "提交"} </span> <span class="changelog-subject">{c.subject}</span> <span class="changelog-meta"> <span class="changelog-hash">{c.hash}</span> <span class="changelog-time">{c.time}</span> </span> </div> )) } </div> </div> ) : ( // 折叠的旧分组 <details class="changelog-collapse"> <summary class="changelog-collapse-summary"> <span class="inline-flex items-center gap-2 text-sm text-(--content-meta) cursor-pointer select-none"> <span class="changelog-collapse-icon">▸</span> <span class="font-medium">{group.date}</span> <span class="text-xs opacity-70">({group.commits.length} 条提交)</span> </span> </summary> <div class="changelog-collapse-body"> <div class="changelog-group mt-2"> <div class="changelog-list"> { group.commits.map((c) => ( <div class="changelog-item"> <span class="changelog-type" style={`color: ${TYPE_META[c.type]?.color ?? "#94a3b8"}; background: color-mix(in srgb, ${TYPE_META[c.type]?.color ?? "#94a3b8"} 12%, transparent);`} > {TYPE_META[c.type]?.label ?? "提交"} </span> <span class="changelog-subject">{c.subject}</span> <span class="changelog-meta"> <span class="changelog-hash">{c.hash}</span> <span class="changelog-time">{c.time}</span> </span> </div> )) } </div> </div> </div> </details> ) ) )) } </div>
<!-- 页脚说明 --> <p class="text-xs text-(--content-meta) mt-8 pt-4 border-t border-(--line-divider)"> 数据由 <code class="text-(--primary)">scripts/generate-changelog.mjs</code> 在构建时从 git 提交历史自动生成。 </p> </div> </div></MainGridLayout>
<style> /* 日期分组 */ .changelog-group { display: flex; flex-direction: column; gap: 0.5rem; }
.changelog-date-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.25rem; }
.changelog-date-badge { display: inline-flex; align-items: center; padding: 0.2rem 0.75rem; border-radius: 9999px; font-size: 0.8rem; font-weight: 600; font-variant-numeric: tabular-nums; color: var(--primary); background: color-mix(in oklch, var(--primary) 12%, transparent); border: 1px solid color-mix(in oklch, var(--primary) 25%, transparent); }
.changelog-date-count { font-size: 0.75rem; color: var(--content-meta); }
/* 提交列表 */ .changelog-list { display: flex; flex-direction: column; border: 1px solid var(--line-divider); border-radius: 0.75rem; overflow: hidden; }
.changelog-item { display: flex; align-items: center; gap: 0.75rem; padding: 0.55rem 0.9rem; font-size: 0.85rem; transition: background 0.2s ease; }
.changelog-item:not(:last-child) { border-bottom: 1px dashed var(--line-divider); }
.changelog-item:hover { background: var(--btn-plain-bg-hover); }
.changelog-type { flex-shrink: 0; display: inline-flex; align-items: center; padding: 0.1rem 0.5rem; border-radius: 0.4rem; font-size: 0.7rem; font-weight: 600; white-space: nowrap; }
.changelog-subject { flex: 1; min-width: 0; font-weight: 500; color: var(--deep-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.changelog-meta { flex-shrink: 0; display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.7rem; font-variant-numeric: tabular-nums; }
.changelog-hash { font-family: ui-monospace, "Cascadia Mono", "Segoe UI Mono", monospace; color: var(--primary); opacity: 0.75; }
.changelog-time { color: var(--content-meta); }
/* 折叠分组 */ .changelog-collapse { border: 1px solid var(--line-divider); border-radius: 0.75rem; overflow: hidden; }
.changelog-collapse-summary { list-style: none; padding: 0.6rem 0.9rem; cursor: pointer; transition: background 0.2s ease; }
.changelog-collapse-summary:hover { background: var(--btn-plain-bg-hover); }
.changelog-collapse-summary::-webkit-details-marker { display: none; }
.changelog-collapse[open] .changelog-collapse-icon { transform: rotate(90deg); }
.changelog-collapse-icon { display: inline-block; transition: transform 0.2s ease; font-size: 0.7rem; }
.changelog-collapse-body { padding: 0 0.75rem 0.75rem; }
/* 暗色适配 */ :global(.dark) .changelog-subject { color: rgba(255, 255, 255, 0.88); }</style>注册页面
打开 src/config/siteConfig.ts,在 pages 页面开关配置(resolvePageToggles({ ... }) 内)添加:
// 更新日志页面开关(自定义页面) changelog: true,打开 src/types/siteConfig.ts,在 pages 类型定义pages: { ... };中添加:
changelog?: boolean; // 更新日志页面开关(用户自定义)添加导航栏入口
打开 src/config/navBarConfig.ts:
① 在 LinkPresets 对象末尾(About 之后)添加:
// 更新日志(自定义页面) Changelog: { name: "更新日志", url: "/changelog/", icon: "material-symbols:history", pageKey: "changelog", },② 在「关于」菜单的 children 数组中添加(顶部):
children: [ // 更新日志 LinkPresets.Changelog,
// 打赏 LinkPresets.Sponsor,
// 关于页面 LinkPresets.About, ],接入构建流程(可选但推荐)
打开 package.json,在 build 最前面加上生成步骤:
"build": "node scripts/generate-changelog.mjs && npx tsx scripts/generate-github-card-data.ts && ..."首次生成数据
博客根目录执行:
node scripts/generate-changelog.mjs会在 src/constants/changelog.json 生成数据(显示「共 N 次提交 · M 个日期」)。
使用说明
提交规范:
提交消息以类型前缀开头即可自动分类
示例:
git commit -m "feat: 新增音乐可视化"git commit -m "fix: 修复天气按钮重叠"git commit -m "perf: 压缩文章图片"| 提交前缀 | 页面显示 | 颜色 | 适用场景 |
|---|---|---|---|
feat: | 新功能 | 绿 | 新页面、新组件、新能力 |
fix: | 修复 | 红 | 修 bug、修样式问题 |
perf: | 优化 | 蓝 | 性能、加载速度 |
improve: | 改进 | 青 | 体验改进(非性能) |
refactor: | 重构 | 橙 | 重写代码,行为不变 |
style: | 样式 | 青蓝 | 纯 CSS/视觉调整 |
docs: | 文档 | 紫 | README、注释、文档 |
chore: | 杂项 | 灰 | 依赖、配置、杂物 |
build: | 构建 | 灰 | 构建脚本/流程 |
merge: | 合并 | 浅青 | 分支合并 |
revert: | 回退 | 红 | 撤销提交 |
| 其他 | 提交 | 灰 | 未匹配的默认分类 |
每次提交都需执行node scripts/generate-changelog.mjs更新日志数据 |
若接入构建流程可由平台部署时自动生成
完整工作流:
# 1. 提交你的改动git add .git commit -m "feat: xxx"
# 2. 刷新更新日志数据(或直接 pnpm build 自动刷新)node scripts/generate-changelog.mjs
# 3. 提交更新后的数据git add src/constants/changelog.jsongit commit -m "chore: 更新更新日志"
git push注意事项
-
脚本读的是当前 git 仓库历史——如果仓库带有上游历史(比如从模板复制),日志会显示上游的提交;想只显示自己的提交,请先重置历史:删除
.git后git init+ 首次提交。 -
Cloudflare Pages / Workers 部署:CF 默认浅克隆(
depth=1),脚本开头会自动git fetch --unshallow拉深历史——只要部署平台执行pnpm build(build 脚本含 generate-changelog),就能在构建时生成最新提交数据,无需本地手动跑。git 环境不可用时会生成空数据(页面显示 0 条),不会导致构建失败。 -
页面默认展开最近 10 组,如需调整改
changelog.astro顶部的EXPANDED_COUNT常量。 -
如需修改分类文案/颜色,同时改脚本
TYPE_LABELS和页面TYPE_META(两处保持一致)。


