前言
Butterfly 默认的分类页和标签页能列出名称,但很难回答三个更实际的问题:这个分类最近写了什么、某个标签常和哪些标签一起出现、我能否直接搜索和排序?因此,这次改造把两个列表升级成了一张“内容地图”。
一、改造后的信息结构
| 页面 | 新增能力 |
|---|---|
| 分类总览 | 分类说明、图标、颜色、文章数量、最近文章、近十二个月活跃度 |
| 标签总览 | 搜索、热度/名称/最近使用排序、关联标签、最近文章 |
| 分类详情 | 横向分类导航、按年份分组的文章列表、全站文章编号 |
| 标签详情 | 标签导航、相关文章、分页、分类和其他标签信息 |
这不是单纯换一套 CSS。页面所需的大部分信息并不存在于原始模板中,必须先在 Hexo 构建阶段计算出来。
二、数据从哪里来
Hexo 生成网站时,会把文章、分类和标签放进本地数据模型:
const posts = hexo.locals.get('posts') |
这些对象看起来像数组,但实际可能是 Hexo 使用的查询集合。为了让后续代码统一处理,先写一个转换函数:
const collectionItems = collection => { |
对新手来说,可以把它理解成一个“适配器”:不管 Hexo 交过来的数据是哪种包装,函数最终都返回普通数组。
三、先构建一次数据快照
分类总览、标签总览和每个详情页都需要文章数据。如果每渲染一个页面就重新统计一次,文章越多,构建时重复工作越多。因此脚本把计算结果缓存成快照:
let cachedSnapshot |
一次 hexo generate 中,buildSnapshot() 只执行一次;下一次重新生成网站前清空缓存,确保新增文章能够进入统计。
快照中保存:
- 按日期排序的全部文章;
- 分类与标签记录;
- 名称到记录的
Map索引; - 全站文章编号;
- 已分类、已打标签的文章数量;
- 最近一次发布日期。
Map 可以看作一本目录。使用分类名查找记录时,不必每次从数组第一项扫描到最后一项。
四、分类外观为什么单独放进 YAML
Hexo 知道分类名称和文章数量,却不知道“建站笔记应该使用代码图标和蓝色”。这些展示信息放在 source/_data/taxonomy.yml:
categories: |
脚本读取配置时还会校验输入:
const safeColor = value => |
这样即使颜色或图标写错,也会回退到安全默认值,不会把错误内容直接插进 style 或 class。
五、近十二个月活跃度怎样计算
每个分类卡片底部有十二根短柱,表示最近十二个月的发布节奏。计算分为三步。
第一步,以最新文章所在月份为终点,生成连续十二个月:
const months = monthWindow(latestPost.date) |
第二步,先把十二个月全部初始化为 0,再遍历分类下的文章累加:
const monthCounts = new Map(months.map(month => [month.key, 0])) |
先补 0 很重要。如果某个月没发文章,它仍应占据时间轴中的一个位置,而不是让相邻月份错误地挨在一起。
第三步,以该分类中最高的月份为 100%,换算柱高和透明度:
const ratio = item.count === 0 ? 0 : item.count / max |
这种归一化展示的是“这个分类自己的发布节奏”,不是不同分类之间的绝对比较。
六、关联标签是怎样算出来的
关联标签没有使用人工配置,而是通过文章中的共同出现次数计算。
假设文章 A 同时包含 Hexo、Butterfly、网站魔改。统计 Hexo 时,另外两个标签的关联次数都加 1。遍历完所有含 Hexo 的文章后,按共同出现次数排序,取前两个:
record.posts.forEach(post => { |
这是一种简单的“共现关系”。它不理解文章语义,但对于博客标签已经足够直观,而且每次构建都会自动更新。
七、标签搜索为什么不需要请求服务器
Hexo 是静态网站,浏览器搜索标签时不能临时查询数据库。因此,构建阶段把可搜索文本写进卡片的 data-* 属性:
<article |
浏览器端收到输入后只做三件事:
const query = normalize(input.value) |
includes()判断是否匹配;hidden控制是否显示;append()把同一个 DOM 元素移动到新顺序,不会复制卡片。
排序时根据下拉框选择文章数量、最近使用时间或中文名称,并设置第二、第三排序条件,避免数值相同时顺序反复变化。
八、总览页与详情页怎样接管原页面
脚本在 after_render:html 阶段判断当前页面类型:
const isCategoriesOverview = page.type === 'categories' |
命中后,脚本生成新的主体 HTML,替换 <main> 内原有内容,并只向这些页面注入专属 CSS 和 JavaScript。
详情页仍然使用 Hexo 已经完成分页的 page.posts,因此不会破坏原来的分类和标签地址。脚本只是改变展示结构,没有重新实现路由系统。
九、PJAX 下为什么要防止重复绑定
Butterfly 使用 PJAX 切换页面时,浏览器不会完整刷新。初始化函数可能执行多次,所以脚本先做标记:
if (!root || root.dataset.taxonomyBound === 'true') return |
随后同时监听首次加载和 PJAX 完成事件:
document.addEventListener('DOMContentLoaded', initBlogTaxonomies) |
没有这个保护,搜索框和排序框可能被重复绑定,操作一次却触发多次刷新。
十、维护入口
- 分类颜色、图标、顺序和说明:
source/_data/taxonomy.yml - 数据整理与页面 HTML:
scripts/blog-taxonomies.js - 搜索、排序和 PJAX 初始化:
source/js/blog-taxonomies.js - 卡片、详情页和移动端样式:
source/css/blog-taxonomies.css
新增普通标签不需要改代码;新增分类时,即使不写 YAML 也能显示,只是使用默认外观。
结语
这次改造的关键不是卡片样式,而是先把 Hexo 的文章关系整理成可复用快照,再让静态页面拥有搜索、排序和关联推荐能力。






