在Hexo-fluid的分类页中增加说明(2)--注入法
之前发过一篇文章:在Hexo-fluid的分类页中增加说明,实现在分类页面增加说明介绍分类,是通过修改主题模板文件实现的,但如果后期主题更新还需要手动再次修改,不太方便,这次实现不修改主题源码,数据驱动的内容注入,只要容器名不变就一直有效。
背景
在使用 Hexo + Fluid 主题时,我需要在分类页(/categories/)顶部添加一个“专栏说明”板块,用于展示不同分类的介绍、图标和自定义颜色。要求:
· 不修改主题源文件(便于后续升级)
· 内容通过配置文件管理(无需改动代码)
· 位置精确(位于分类列表之前)
经过探索,最终采用 Hexo 官方的 after_render:html 过滤器实现。下面分享完整的实现过程。
最终效果参见本站的分类页
一、整体思路
- 监听生成事件:使用 after_render:html 过滤器,在每个 HTML 文件生成后执行。
- 识别目标页面:判断当前文件路径是否为 categories/index.html。
- 注入样式:在
</head> 前插入专栏卡片所需的 CSS。 - 读取数据:从 source/_data/ 目录下的 YAML 文件中读取专栏配置。
- 生成 HTML:遍历数据,生成与主题结构兼容的卡片 HTML。
- 插入到正确位置:通过正则匹配分类列表容器(
<div class="category-list">),将卡片 HTML 插入其前面。 - 返回修改后的内容。
二、图标字体准备(两种方案可选)
专栏卡片需要用到一些小图标。你可以根据自己的情况,选择下面任意一种方式。
方案 A:使用主题自带的图标
Fluid 主题内置了一套图标库(基于 iconfont 或类似方案)。如果你觉得里面的图标够用,那么不需要引入任何额外的字体文件。
只需要在数据文件(column_descriptions.yml)中填写主题已有的图标类名,比如 iconfont icon-code 或 ri-code-line(具体取决于你主题的图标体系),并在生成卡片的 HTML 中使用对应的 <i> 标签即可。
后续代码中的卡片生成部分已经为自定义图标类名预留了位置,你可以自由替换为 col.icon 的值。
方案 B:使用 Fontello 生成自定义图标包
如果你觉得主题图标不够个性,或担心类名冲突,我推荐使用 Fontello 来生成一套专属于专栏的图标字体。这样既能自由挑选图标,又能通过自定义前缀(比如 fi-)与主题图标彻底隔离。
操作步骤:
- 挑选图标
访问 fontello.com,搜索你需要的图标(如 desktop、newspaper、code 等),点击选中。 - 自定义前缀
点击右上角的 扳手图标,在 “CSS class prefix” 里把默认的 icon- 改成 fi-(可自定义,但要和脚本中的类名匹配)。
为什么要改? 因为主题中可能已经有 icon- 开头的类名,如果 Fontello 也输出同样的前缀,主题的图标就会被覆盖成乱码。换成 fi- 后互不干扰。 - 下载并放置文件
下载压缩包,解压后你会得到 css/font-awesome.css、css/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 └── ...(其他字体文件)
|
- 选择 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" description: "分享前端、后端、DevOps 等技术实践。" color: "#1890ff" - name: "生活杂谈" subtitle: "日常思考与感悟" icon: "fi-pen" description: "记录生活中的点滴与读书笔记。" color: "#f5222d"
|
name:分类名称,会自动拼接“专栏”二字(可以按需去掉)。icon:图标类名,必须与你选择方案中的类名一致(如 fi-code、iconfont 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); }
.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; }
.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; } }
@supports not (mask-image: none) { .teach-column-desc::before { background: none; } }
|
五、编写注入脚本
在博客根目录下新建 scripts/categories-inject.js,它是整个方案的核心。主要做这几件事:
- 仅处理
categories/index.html - 如果检测到页面中还没有加载
custom.css,则向 </head> 前插入样式链接 - 如果你选择了方案 B 且尚未加载 Fontello 的 CSS,可以在此处注入 Fontello 样式(脚本中已预留注释,启用即可)
- 读取
source/_data/column_descriptions.yml,将数据转为卡片 HTML - 通过正则找到
<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
| 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;
if (!str.includes('/css/custom.css')) { str = str.replace('</head>', '<link rel="stylesheet" href="/css/custom.css"></head>'); }
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;
const cardsHtml = generateCardsHtml(columns);
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 { str = str.replace('<body>', '<body>\n' + cardsHtml); }
return str; });
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 '&'; if (m === '<') return '<'; if (m === '>') return '>'; 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/ │ ├── css/ │ │ └── font-awesome.css │ └── font/ └── ……
|
鸣心/Write