前言

一个持续维护的博客,从来不只是文章数量在增加。主题会升级,页面会重做,导航会调整,也会有许多只有站长自己知道的修复与取舍。

过去,这些变化散落在 Git 提交、配置文件和记忆里。访客只能看到今天的样子,却不知道它经历过什么;过一段时间后,连维护者自己也很难准确回答“这个功能是什么时候加上的”。因此,这次改造为博客增加了一个独立的更新记录页,把重要变化整理成一条可以回看的时间线。

这也是“Butterfly 建站笔记”系列的第一篇。本文不会只罗列最终代码,而是从构想、数据组织和渲染过程出发,完整记录这个页面是怎样被设计出来的。

一、构想:更新记录不应该是一篇普通文章

最直接的做法,是新建一篇名为“更新记录”的 Markdown 文章,然后不断往下追加内容。但这种方案很快会遇到几个问题:

  • 年份和日期需要手工维护,内容一多就难以浏览;
  • “新增、优化、修复、升级”等变化没有统一的视觉层级;
  • 统计年份、节点和变化数量时,需要人工反复计算;
  • 内容和页面结构混在一起,稍微调整样式就可能碰到记录本身;
  • 更新记录作为长期入口,不应该随着普通文章一起出现在首页时间流中。

所以,这次改造没有把它做成普通文章,而是把它拆成了三个各司其职的部分:

source/_data/updates.yml      负责保存更新数据
scripts/blog-updates.js 负责整理数据并生成页面结构
source/css/blog-updates.css 负责视觉样式与响应式适配

页面文件 source/updates/index.md 反而非常轻,只保留页面配置和一个自定义标签:

---
title: 更新记录
layout: page
permalink: /updates/
comments: false
aside: false
toc: false
---
{% blog_updates %}

这样,更新记录拥有固定的 /updates/ 地址,却不会被当作一篇普通博客文章。

二、改造前后有什么变化

改造前,博客的重要变化只能从提交记录或零散说明中寻找。它们对访客不可见,也缺少统一的归档方式。

改造后的更新记录页提供了以下信息:

改造前 改造后
没有独立的更新入口 导航栏可以直接进入 /updates/
变化散落在 Git 提交中 按年份、日期形成时间线
不同类型的变化没有区分 使用“新增、优化、修复、升级”等彩色徽标
历史内容增多后难以浏览 年份支持折叠,默认展开最近一年
数量需要人工统计 自动计算年份、节点和变化总数
页面样式与数据耦合 YAML、渲染逻辑和 CSS 分离维护

页面顶部先给出总体统计,主体再按年份组织。年份内部使用时间线串联每个日期,既能快速扫读,又能保留博客逐步成长的感觉。

查看当前更新记录页

三、用 YAML 保存更新内容

更新记录最常修改的其实不是页面,而是数据。因此,我把所有记录放进 source/_data/updates.yml

intro: 记录博客功能、界面与部署方式的重要变化。

years:
- year: 2026
open: true
entries:
- date: 2026.07.16
items:
- type: 新增
text: 上线更新记录页面。
- type: 升级
text: Butterfly 主题升级至 5.6.0

这套结构分为三层:

  1. year 表示年份,open 决定页面打开时是否默认展开;
  2. entries 保存这一年的日期节点;
  3. items 保存当天发生的具体变化。

以后新增一条记录,只需要编辑 YAML,不需要再碰 HTML 和 CSS。Hexo 会在生成网站时读取 _data 目录,并把内容交给渲染脚本。

四、用 Hexo 自定义标签生成时间线

Hexo 会自动加载项目根目录 scripts 下的脚本。更新记录页利用这一机制注册了一个 blog_updates 标签:

hexo.extend.tag.register('blog_updates', updatesMarkup)

当 Hexo 在页面中遇到 {% blog_updates %} 时,就会执行 updatesMarkup(),读取 updates.yml 并生成最终结构。

核心流程可以概括为:

读取 updates.yml

复制并排序年份与日期

把每项变化转换为安全的 HTML

计算年份、节点和变化总数

输出页面头部、时间线和折叠年份

1. 排序时不直接修改原始数据

脚本先复制数组,再进行排序:

const years = Array.isArray(data.years) ? [...data.years] : []
years.sort((a, b) => Number(b.year || 0) - Number(a.year || 0))

日期节点也采用同样的方式处理。这样不会在渲染过程中改变 Hexo 已经加载的数据,其他插件读取同一份数据时仍能得到原来的结构。

2. 对内容进行 HTML 转义

虽然更新数据由自己维护,但把文本插入 HTML 前仍然进行转义更稳妥:

const escapeHtml = value => String(value ?? '')
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;')

这可以避免某条记录中的特殊字符意外破坏页面结构,也为未来从其他来源导入记录留下安全余量。

3. 让类型决定徽标样式

不同更新类型会映射到不同的 CSS 类名:

const TYPE_CLASS = Object.freeze({
上线: 'launch',
新增: 'new',
优化: 'optimize',
修复: 'fix',
迁移: 'migrate',
升级: 'change',
调整: 'change'
})

数据只需要写“新增”或“修复”,脚本就会自动为它选择对应颜色。未知类型也会落入通用样式,不会导致渲染失败。

五、为什么使用原生 details 元素

年份折叠没有依赖额外的 JavaScript,而是使用浏览器原生的 <details><summary>

<details class="blog-update-year" open>
<summary>2026</summary>
<div class="blog-update-year__body">...</div>
</details>

这么做有三个好处:

  • 即使脚本加载失败,折叠功能依然可用;
  • 键盘操作和基础可访问性由浏览器提供;
  • 不需要再维护点击事件和展开状态。

视觉上的箭头旋转、圆角变化和时间线动画,则全部交给 CSS 完成。

六、样式设计:跟随主题,而不是覆盖主题

更新记录页定义了一组局部变量:

.blog-updates {
--updates-accent: var(--theme-color, #1677b3);
--updates-line: rgba(22, 119, 179, 0.24);
--updates-soft: rgba(22, 119, 179, 0.08);
--updates-border: rgba(127, 127, 127, 0.18);
}

主色优先继承 Butterfly 的 --theme-color,卡片背景和文字颜色也使用主题已有变量。这样切换主题色或深色模式时,更新记录页可以自然跟随,而不需要复制一套独立配色。

桌面端顶部同时展示标题和统计信息;在窄屏设备上,统计区会自动换行,时间线左右间距也会相应缩小。这里的目标并不是做一个脱离博客风格的炫技页面,而是让它看起来像 Butterfly 原本就拥有的功能。

七、只在需要时加载页面样式

如果把更新记录 CSS 加进全站配置,那么每个页面都会请求一份只有 /updates/ 才会使用的样式。为此,脚本在 HTML 渲染完成后检查页面内容,只对真正包含更新记录的页面注入样式表:

hexo.extend.filter.register('after_render:html', html => {
if (!html.includes('class="blog-updates"') || !html.includes('</head>')) return html

const stylesheet = '<link rel="stylesheet" href="/css/blog-updates.css?v=20260716-1">'
return html.replace('</head>', `${stylesheet}</head>`)
}, 30)

URL 后面的版本号也很重要。浏览器或 CDN 长期缓存旧 CSS 时,只要更新版本号,就能让访客及时获取新样式,而不必要求他们手动清理缓存。

八、以后怎样维护

日常维护只需要关注 source/_data/updates.yml

  • 新的一天:在对应年份的 entries 中增加日期节点;
  • 同一天的新变化:继续向当天的 items 追加内容;
  • 新的一年:新增一个年份,并把 open: true 移到最新年份;
  • 新的变化类型:在脚本的 TYPE_CLASS 和 CSS 中增加一组映射;
  • 修改视觉效果:只编辑 source/css/blog-updates.css

更新记录应当记录对访客或维护流程有实际意义的变化,而不是机械复制每一条 Git 提交。一次功能可能经历多次调试,最终在时间线上保留一句准确、可理解的结果就够了。

结语

更新记录页并不是博客里最复杂的功能,却让一次次零散修改开始形成连续的历史。它既是写给访客的说明,也是留给未来自己的维护档案。

更重要的是,这次实现确立了一种适合后续魔改的方式:数据、结构和样式彼此分离;优先使用浏览器原生能力;只在需要的页面加载资源;同时尽量服从 Butterfly 已有的主题系统。

接下来的“Butterfly 建站笔记”还会继续记录分类与标签内容地图、折叠归档、文章统计、首页摄影轮播等功能的设计与实现。