正在加载
视频加载失败

为Firefly博客加入「更新日志」页面,记录并展示每一次的Git提交修改

2300 字
12 分钟
为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.jsonbuild 脚本开头接入生成步骤

添加步骤#

放入生成脚本#

在你的博客/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 && ..."

首次生成数据#

博客根目录执行:

Terminal window
node scripts/generate-changelog.mjs

会在 src/constants/changelog.json 生成数据(显示「共 N 次提交 · M 个日期」)。

使用说明#

提交规范:

提交消息以类型前缀开头即可自动分类

示例:

Terminal window
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更新日志数据

若接入构建流程可由平台部署时自动生成

完整工作流:

Terminal window
# 1. 提交你的改动
git add .
git commit -m "feat: xxx"
# 2. 刷新更新日志数据(或直接 pnpm build 自动刷新)
node scripts/generate-changelog.mjs
# 3. 提交更新后的数据
git add src/constants/changelog.json
git commit -m "chore: 更新更新日志"
git push

注意事项#

  1. 脚本读的是当前 git 仓库历史——如果仓库带有上游历史(比如从模板复制),日志会显示上游的提交;想只显示自己的提交,请先重置历史:删除 .git 后 git init + 首次提交。

  2. Cloudflare Pages / Workers 部署:CF 默认浅克隆(depth=1),脚本开头会自动 git fetch --unshallow 拉深历史——只要部署平台执行 pnpm build(build 脚本含 generate-changelog),就能在构建时生成最新提交数据,无需本地手动跑。git 环境不可用时会生成空数据(页面显示 0 条),不会导致构建失败。

  3. 页面默认展开最近 10 组,如需调整改 changelog.astro 顶部的 EXPANDED_COUNT 常量。

  4. 如需修改分类文案/颜色,同时改脚本 TYPE_LABELS 和页面 TYPE_META(两处保持一致)。

为Firefly博客加入「更新日志」页面,记录并展示每一次的Git提交修改
https://www.jerrynis.com/posts/add-change-log/
作者
Jerrynis
发布于
2026-08-29
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Jerrynis
I'm a mouse.
正在获取时间...
--:--
---
--/--
分类
标签
文章目录