在Hexo-fluid的分类页中增加说明(2)--注入法

本文最后更新于 2026年7月20日 下午

在Hexo-fluid的分类页中增加说明(2)--注入法

之前发过一篇文章:在Hexo-fluid的分类页中增加说明,实现在分类页面增加说明介绍分类,是通过修改主题模板文件实现的,但如果后期主题更新还需要手动再次修改,不太方便,这次实现不修改主题源码,数据驱动的内容注入,只要容器名不变就一直有效。

背景

在使用 Hexo + Fluid 主题时,我需要在分类页(/categories/)顶部添加一个“专栏说明”板块,用于展示不同分类的介绍、图标和自定义颜色。要求:

· 不修改主题源文件(便于后续升级)
· 内容通过配置文件管理(无需改动代码)
· 位置精确(位于分类列表之前)

经过探索,最终采用 Hexo 官方的 after_render:html 过滤器实现。下面分享完整的实现过程。

最终效果参见本站的分类页

一、整体思路

  1. 监听生成事件:使用 after_render:html 过滤器,在每个 HTML 文件生成后执行。
  2. 识别目标页面:判断当前文件路径是否为 categories/index.html。
  3. 注入样式:在 </head> 前插入专栏卡片所需的 CSS。
  4. 读取数据:从 source/_data/ 目录下的 YAML 文件中读取专栏配置。
  5. 生成 HTML:遍历数据,生成与主题结构兼容的卡片 HTML。
  6. 插入到正确位置:通过正则匹配分类列表容器(<div class="category-list">),将卡片 HTML 插入其前面。
  7. 返回修改后的内容。

二、图标字体准备(两种方案可选)

专栏卡片需要用到一些小图标。你可以根据自己的情况,选择下面任意一种方式。

方案 A:使用主题自带的图标

Fluid 主题内置了一套图标库(基于 iconfont 或类似方案)。如果你觉得里面的图标够用,那么不需要引入任何额外的字体文件
只需要在数据文件(column_descriptions.yml)中填写主题已有的图标类名,比如 iconfont icon-coderi-code-line(具体取决于你主题的图标体系),并在生成卡片的 HTML 中使用对应的 <i> 标签即可。
后续代码中的卡片生成部分已经为自定义图标类名预留了位置,你可以自由替换为 col.icon 的值。

方案 B:使用 Fontello 生成自定义图标包

如果你觉得主题图标不够个性,或担心类名冲突,我推荐使用 Fontello 来生成一套专属于专栏的图标字体。这样既能自由挑选图标,又能通过自定义前缀(比如 fi-)与主题图标彻底隔离。

操作步骤:

  1. 挑选图标
    访问 fontello.com,搜索你需要的图标(如 desktopnewspapercode 等),点击选中。
  2. 自定义前缀
    点击右上角的 扳手图标,在 “CSS class prefix” 里把默认的 icon- 改成 fi-(可自定义,但要和脚本中的类名匹配)。
    为什么要改? 因为主题中可能已经有 icon- 开头的类名,如果 Fontello 也输出同样的前缀,主题的图标就会被覆盖成乱码。换成 fi- 后互不干扰。
  3. 下载并放置文件
    下载压缩包,解压后你会得到 css/font-awesome.csscss/font-awesome-embedded.css 以及字体文件夹。
    把整个解压目录放到博客的 source/libs/fontello/ 下,最终结构如:
    1
    2
    3
    4
    5
    6
    7
    source/libs/fontello/
    ├── css/
    │ ├── font-awesome.css
    │ └── font-awesome-embedded.css
    └── font/
    ├── fontello.woff2
    └── ...(其他字体文件)
  4. 选择 CSS 加载方式
    • font-awesome.css:通过 @font-face 外部引用字体文件。浏览器先下载 CSS,再下载字体文件,文件小,适合浏览器缓存。
    • font-awesome-embedded.css:将字体以 Base64 直接嵌入 CSS 中。仅一个文件,零额外请求,但 CSS 体积稍大(约 30~40 KB)。
      两者选其一即可,切勿同时加载!我在后续脚本中会使用外部引用的版本(font-awesome.css),你如果想换成嵌入式,只需修改注入的路径。

重要提醒:无论选用方案 A 还是方案 B,后面卡片生成脚本中图标 <i>class 都会直接取自 YAML 里的 icon 字段。因此,数据文件里的图标类名必须与你实际使用的图标库匹配。例如使用 Fontello 时,icon 字段应写成 fi-code 这样的形式,而不是 fas fa-code

三、创建数据文件

source/_data/ 目录下新建 column_descriptions.yml,内容格式如下(请根据自己的分类和图标库调整 icon 字段):

1
2
3
4
5
6
7
8
9
10
11
columns:
- name: "技术随笔"
subtitle: "编程与开发心得"
icon: "fi-code" # Fontello 图标;如果使用主题图标则换成对应类名
description: "分享前端、后端、DevOps 等技术实践。"
color: "#1890ff"
- name: "生活杂谈"
subtitle: "日常思考与感悟"
icon: "fi-pen"
description: "记录生活中的点滴与读书笔记。"
color: "#f5222d"
  • name:分类名称,会自动拼接“专栏”二字(可以按需去掉)。
  • icon:图标类名,必须与你选择方案中的类名一致(如 fi-codeiconfont icon-code 等)。
  • color:用于卡片边框、图标和高亮色。

四、编写卡片样式

为了让卡片好看,我们在 source/css/ 下创建 custom.css,定义一套响应式卡片风格。

** custom.css **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/* 专栏容器 */
.columns-container {
display: grid;
gap: 1.8rem;
margin: 2.5rem 0;
}

/* 单个专栏样式 */
.teach-column-desc {
--column-color: #1890ff;
--column-color-rgb: 24, 144, 255;
--column-hover-color: #40a9ff;

padding: 1.8rem;
border-radius: 12px;
border-left: 4px solid var(--column-color);
background:
radial-gradient(circle at 90% 10%, rgba(var(--column-color-rgb), 0.03) 0%, transparent 20%),
linear-gradient(to bottom, rgba(var(--column-color-rgb), 0.02), rgba(var(--column-color-rgb), 0.01)),
#ffffff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
transition:
transform 0.35s ease,
box-shadow 0.35s ease,
border-color 0.3s ease;
position: relative;
overflow: hidden;
}

/* 悬停效果 */
.teach-column-desc:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-left-color: var(--column-hover-color);
}

/* 背景图案(内嵌SVG,不发起网络请求) */
.teach-column-desc::before {
content: "";
position: absolute;
top: -20px;
right: -20px;
width: 80px;
height: 80px;
opacity: 0.05;
background-color: var(--column-color);
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='currentColor' d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z'/%3E%3C/svg%3E");
mask-repeat: no-repeat;
z-index: -1;
}

.desc-header {
display: flex;
align-items: flex-start;
margin-bottom: 1.2rem;
}

.desc-header i {
font-size: 2.2rem;
margin-right: 15px;
flex-shrink: 0;
color: var(--column-color);
transition: color 0.3s ease;
}

.teach-column-desc:hover .desc-header i {
color: var(--column-hover-color);
}

.desc-header h3 {
margin: 0 0 5px 0;
font-size: 1.45rem;
font-weight: 600;
color: var(--column-color);
transition: color 0.3s ease;
}

.teach-column-desc:hover .desc-header h3 {
color: var(--column-hover-color);
}

.subtitle {
margin: 0;
font-size: 0.95rem;
color: #666;
font-style: italic;
}

.desc-content p {
margin: 0;
line-height: 1.85;
font-size: 1.08rem;
color: #333;
position: relative;
padding-left: 1.8rem;
}

.desc-content p::before {
content: "";
position: absolute;
left: 0;
top: 0.6em;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--column-color);
opacity: 0.7;
transition: background 0.3s ease;
}

.teach-column-desc:hover .desc-content p::before {
background: var(--column-hover-color);
}

.highlight {
color: var(--column-color);
font-weight: 600;
position: relative;
display: inline-block;
margin-right: 5px;
transition: color 0.3s ease;
}

.teach-column-desc:hover .highlight {
color: var(--column-hover-color);
}

.highlight::after {
content: "";
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: currentColor;
opacity: 0.3;
border-radius: 2px;
transition: opacity 0.3s ease;
}

.teach-column-desc:hover .highlight::after {
opacity: 0.4;
}

/* 默认图标兜底(YAML 未填 icon 时使用) */
.card-default-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.2rem;
height: 2.2rem;
margin-right: 15px;
flex-shrink: 0;
font-size: 0;
}

.card-default-icon::before {
content: "";
display: block;
width: 0.65rem;
height: 0.65rem;
border-radius: 50%;
background-color: var(--column-color);
transition: background-color 0.3s ease;
}

.teach-column-desc:hover .card-default-icon::before {
background-color: var(--column-hover-color);
}

/* 响应式设计 */
@media (max-width: 768px) {
.columns-container {
gap: 1.3rem;
}

.teach-column-desc {
padding: 1.3rem;
}

.desc-header {
flex-direction: column;
align-items: flex-start;
}

.desc-header i,
.card-default-icon {
margin-bottom: 10px;
margin-right: 0;
}
}

/* 为不支持 mask-image 的浏览器提供回退 */
@supports not (mask-image: none) {
.teach-column-desc::before {
background: none;
}
}

五、编写注入脚本

在博客根目录下新建 scripts/categories-inject.js,它是整个方案的核心。主要做这几件事:

  1. 仅处理 categories/index.html
  2. 如果检测到页面中还没有加载 custom.css,则向 </head> 前插入样式链接
  3. 如果你选择了方案 B 且尚未加载 Fontello 的 CSS,可以在此处注入 Fontello 样式(脚本中已预留注释,启用即可)
  4. 读取 source/_data/column_descriptions.yml,将数据转为卡片 HTML
  5. 通过正则找到 <div class="category-list"> 的位置,把卡片 HTML 插到它前面

** categories-inject.js **

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// scripts/categories-inject.js
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

hexo.extend.filter.register('after_render:html', function(str, data) {
// 仅处理分类页
if (data.path !== 'categories/index.html') return str;

// 1. 注入卡片样式(路径已改为 /css/custom.css)
if (!str.includes('/css/custom.css')) {
str = str.replace('</head>', '<link rel="stylesheet" href="/css/custom.css"></head>');
}

// 2. (可选)如果使用了 Fontello 图标包且主题未加载,可取消下面几行注释来注入
// if (!str.includes('/libs/fontello/css/font-awesome.css')) {
// str = str.replace('</head>', '<link rel="stylesheet" href="/libs/fontello/css/font-awesome.css"></head>');
// }

// 3. 读取专栏数据
const dataPath = path.join(hexo.source_dir, '_data', 'column_descriptions.yml');
let columns = [];
if (fs.existsSync(dataPath)) {
try {
const content = fs.readFileSync(dataPath, 'utf8');
const yamlData = yaml.load(content);
columns = yamlData && yamlData.columns ? yamlData.columns : [];
} catch (e) {
console.error('读取专栏数据失败:', e);
}
}

if (!columns.length) return str;

// 4. 生成专栏卡片 HTML
const cardsHtml = generateCardsHtml(columns);

// 5. 插入到分类列表之前
const categoryListRegex = /<div\s+class="category-list"[^>]*>/i;
const match = str.match(categoryListRegex);
if (match) {
const insertPosition = match.index;
str = str.slice(0, insertPosition) + cardsHtml + str.slice(insertPosition);
} else {
// 回退:如果找不到分类列表容器,放在 <body> 之后
str = str.replace('<body>', '<body>\n' + cardsHtml);
}

return str;
});

// 生成卡片 HTML(类名完全与 custom.css 匹配)
function generateCardsHtml(columns) {
let html = '<div class="columns-container">';
for (const col of columns) {
const color = col.color || '#1890ff';
const rgb = hexToRgb(color);
html += `
<div class="teach-column-desc" style="--column-color: ${color}; --column-color-rgb: ${rgb.join(',')};">
<div class="desc-header">
${col.icon
? `<i class="${escapeHtml(col.icon)}"></i>`
: '<span class="card-default-icon"></span>'}
<div>
<h3>${escapeHtml(col.name)}专栏</h3>
${col.subtitle ? `<p class="subtitle">${escapeHtml(col.subtitle)}</p>` : ''}
</div>
</div>
<div class="desc-content">
<p>${escapeHtml(col.description)}</p>
</div>
</div>
`;
}
html += '</div>';
return html;
}

function hexToRgb(hex) {
hex = hex.replace('#', '');
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return [r, g, b];
}

function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}

为了解析配置文件,需要安装js-yaml依赖

1
npm install js-yaml --save

六、测试与验证

执行以下命令:

1
hexo clean && hexo generate

然后检查 public/categories/index.html

  • <head> 中是否包含了 <link rel="stylesheet" href="/css/custom.css">
  • <div class="category-list"> 之前是否出现了 .columns-container 结构

启动本地预览 hexo server,访问分类页就能看到效果。

七、总结

这种基于 after_render:html 过滤器的数据驱动注入方式,彻底告别了对主题模板的修改。所有配置都在 YAML 文件里,样式独立存放,脚本只负责拼装和插入。之后无论 Fluid 主题更新多少次,只要分类列表的 div.category-list 容器不变,我们的专栏卡片就能安稳地待在页面上。万一将来主题把容器标签从 div 改成 section 或加更多属性,只需微调正则即可适配。

同样的思路还可以复用到标签页、归档页,甚至给任意页面插入自定义模块,非常灵活。

附:完整文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
your-blog/
├── scripts/
│ └── categories-inject.js # 注入脚本
├── source/
│ ├── _data/
│ │ └── column_descriptions.yml # 专栏说明数据
│ ├── css/
│ │ └── custom.css # 卡片样式
│ └── libs/ # (如果使用 Fontello)
│ └── fontello/
│ ├── css/
│ │ └── font-awesome.css
│ └── font/
└── ……

 

鸣心/Write

在Hexo-fluid的分类页中增加说明(2)--注入法
https://b.wihi.top/posts/2da73f2a.html
作者
鸣心
发布于
2026年4月4日
更新于
2026年7月20日
许可协议