前言
一个持续维护的博客,从来不只是文章数量在增加。主题会升级,页面会重做,导航会调整,也会有许多只有站长自己知道的修复与取舍。
过去,这些变化散落在 Git 提交、配置文件和记忆里。访客只能看到今天的样子,却不知道它经历过什么;过一段时间后,连维护者自己也很难准确回答“这个功能是什么时候加上的”。因此,这次改造为博客增加了一个独立的更新记录页,把重要变化整理成一条可以回看的时间线。
这也是“Butterfly 建站笔记”系列的第一篇。本文不会只罗列最终代码,而是从构想、数据组织和渲染过程出发,完整记录这个页面是怎样被设计出来的。
一、构想:更新记录不应该是一篇普通文章
最直接的做法,是新建一篇名为“更新记录”的 Markdown 文章,然后不断往下追加内容。但这种方案很快会遇到几个问题:
- 年份和日期需要手工维护,内容一多就难以浏览;
- “新增、优化、修复、升级”等变化没有统一的视觉层级;
- 统计年份、节点和变化数量时,需要人工反复计算;
- 内容和页面结构混在一起,稍微调整样式就可能碰到记录本身;
- 更新记录作为长期入口,不应该随着普通文章一起出现在首页时间流中。
所以,这次改造没有把它做成普通文章,而是把它拆成了三个各司其职的部分:
source/_data/updates.yml 负责保存更新数据 |
页面文件 source/updates/index.md 反而非常轻,只保留页面配置和一个自定义标签:
--- |
这样,更新记录拥有固定的 /updates/ 地址,却不会被当作一篇普通博客文章。
二、改造前后有什么变化
改造前,博客的重要变化只能从提交记录或零散说明中寻找。它们对访客不可见,也缺少统一的归档方式。
改造后的更新记录页提供了以下信息:
| 改造前 | 改造后 |
|---|---|
| 没有独立的更新入口 | 导航栏可以直接进入 /updates/ |
| 变化散落在 Git 提交中 | 按年份、日期形成时间线 |
| 不同类型的变化没有区分 | 使用“新增、优化、修复、升级”等彩色徽标 |
| 历史内容增多后难以浏览 | 年份支持折叠,默认展开最近一年 |
| 数量需要人工统计 | 自动计算年份、节点和变化总数 |
| 页面样式与数据耦合 | YAML、渲染逻辑和 CSS 分离维护 |
页面顶部先给出总体统计,主体再按年份组织。年份内部使用时间线串联每个日期,既能快速扫读,又能保留博客逐步成长的感觉。
查看当前更新记录页三、用 YAML 保存更新内容
更新记录最常修改的其实不是页面,而是数据。因此,我把所有记录放进 source/_data/updates.yml:
intro: 记录博客功能、界面与部署方式的重要变化。 |
这套结构分为三层:
year表示年份,open决定页面打开时是否默认展开;entries保存这一年的日期节点;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 |
1. 排序时不直接修改原始数据
脚本先复制数组,再进行排序:
const years = Array.isArray(data.years) ? [...data.years] : [] |
日期节点也采用同样的方式处理。这样不会在渲染过程中改变 Hexo 已经加载的数据,其他插件读取同一份数据时仍能得到原来的结构。
2. 对内容进行 HTML 转义
虽然更新数据由自己维护,但把文本插入 HTML 前仍然进行转义更稳妥:
const escapeHtml = value => String(value ?? '') |
这可以避免某条记录中的特殊字符意外破坏页面结构,也为未来从其他来源导入记录留下安全余量。
3. 让类型决定徽标样式
不同更新类型会映射到不同的 CSS 类名:
const TYPE_CLASS = Object.freeze({ |
数据只需要写“新增”或“修复”,脚本就会自动为它选择对应颜色。未知类型也会落入通用样式,不会导致渲染失败。
五、为什么使用原生 details 元素
年份折叠没有依赖额外的 JavaScript,而是使用浏览器原生的 <details> 和 <summary>:
<details class="blog-update-year" open> |
这么做有三个好处:
- 即使脚本加载失败,折叠功能依然可用;
- 键盘操作和基础可访问性由浏览器提供;
- 不需要再维护点击事件和展开状态。
视觉上的箭头旋转、圆角变化和时间线动画,则全部交给 CSS 完成。
六、样式设计:跟随主题,而不是覆盖主题
更新记录页定义了一组局部变量:
.blog-updates { |
主色优先继承 Butterfly 的 --theme-color,卡片背景和文字颜色也使用主题已有变量。这样切换主题色或深色模式时,更新记录页可以自然跟随,而不需要复制一套独立配色。
桌面端顶部同时展示标题和统计信息;在窄屏设备上,统计区会自动换行,时间线左右间距也会相应缩小。这里的目标并不是做一个脱离博客风格的炫技页面,而是让它看起来像 Butterfly 原本就拥有的功能。
七、只在需要时加载页面样式
如果把更新记录 CSS 加进全站配置,那么每个页面都会请求一份只有 /updates/ 才会使用的样式。为此,脚本在 HTML 渲染完成后检查页面内容,只对真正包含更新记录的页面注入样式表:
hexo.extend.filter.register('after_render:html', html => { |
URL 后面的版本号也很重要。浏览器或 CDN 长期缓存旧 CSS 时,只要更新版本号,就能让访客及时获取新样式,而不必要求他们手动清理缓存。
八、以后怎样维护
日常维护只需要关注 source/_data/updates.yml:
- 新的一天:在对应年份的
entries中增加日期节点; - 同一天的新变化:继续向当天的
items追加内容; - 新的一年:新增一个年份,并把
open: true移到最新年份; - 新的变化类型:在脚本的
TYPE_CLASS和 CSS 中增加一组映射; - 修改视觉效果:只编辑
source/css/blog-updates.css。
更新记录应当记录对访客或维护流程有实际意义的变化,而不是机械复制每一条 Git 提交。一次功能可能经历多次调试,最终在时间线上保留一句准确、可理解的结果就够了。
结语
更新记录页并不是博客里最复杂的功能,却让一次次零散修改开始形成连续的历史。它既是写给访客的说明,也是留给未来自己的维护档案。
更重要的是,这次实现确立了一种适合后续魔改的方式:数据、结构和样式彼此分离;优先使用浏览器原生能力;只在需要的页面加载资源;同时尽量服从 Butterfly 已有的主题系统。
接下来的“Butterfly 建站笔记”还会继续记录分类与标签内容地图、折叠归档、文章统计、首页摄影轮播等功能的设计与实现。






