一、背景
在使用 Hexo + Fluid 主题搭建博客的过程中,我希望在首页 banner 的 subtitle(副标题)下方添加一行站点描述(description)。
最初的想法很简单——直接修改主题的模板文件,但这样做有几个问题:
- 主题升级困难:每次更新主题都需要重新修改源码
- 维护成本高:修改点分散,时间久了容易忘记
- 版本管理混乱:修改 node_modules 下的主题文件,Git 无法跟踪
好在 Hexo 5 提供了**注入器(Injector)**功能,Fluid 主题也有自己的注入机制,可以在不修改主题源码的情况下实现自定义。
本文记录了从零开始实现这个需求的完整过程。
二、探索过程
2.1 找到主题文件位置
Fluid 主题是通过 npm 安装的,所以主题文件位于 node_modules/hexo-theme-fluid/ 目录下:
1 2 3 4 5 6 7 8
| node_modules/hexo-theme-fluid/ ├── layout/ │ ├── index.ejs │ └── _partials/ │ ├── header/ │ │ └── banner.ejs │ └── header.ejs └── source/
|
怎么找到的? 先从 package.json 看到依赖是 hexo-theme-fluid,然后在 _config.yml 中确认 theme: fluid,接着就在 node_modules 下找到了主题目录。
2.2 定位 subtitle 所在的模板
首页对应的模板是 layout/index.ejs,打开后发现它只是设置了一些页面变量,真正的 banner 渲染在 _partials/header/banner.ejs 中:
1 2 3 4 5 6 7 8 9
| <div class="banner-text text-center fade-in-up"> <div class="h2"> <span id="subtitle"><%- subtitle %></span> </div> <% if (is_post()) { %> <%- inject_point('postMetaTop') %> <% } %> </div>
|
关键发现:
id="subtitle" 的 <span> 元素就是副标题
- Fluid 提供了
inject_point('postMetaTop') 注入点,但只在文章页(is_post())生效
- 首页 banner 的 subtitle 下方没有注入点!
2.3 确定注入方式
既然 Fluid 主题的注入点不覆盖首页 subtitle 区域,就需要改用 Hexo 原生注入器。
Hexo 原生注入器支持四个位置:
| 注入位置 |
说明 |
head_begin |
<head> 标签之后 |
head_end |
</head> 标签之前 |
body_begin |
<body> 标签之后 |
body_end |
</body> 标签之前 |
我的方案是:
- CSS 注入到
head_end(保证样式在页面渲染前加载)
- JavaScript 注入到
body_end(保证 DOM 已加载)
- 通过 JS 动态创建 description 元素并插入到 subtitle 下方
2.4 如何获取 description
_config.yml 中已经定义了 description 字段:
1
| description: '记录和分享个人学习和工作过程中遇到的问题和解决方法'
|
在 Hexo 的脚本文件(scripts/ 目录下的 js)中,可以通过 hexo.config.description 直接访问这个配置值。
三、实现方案
3.1 创建脚本文件
在博客根目录创建 scripts/ 文件夹(如不存在),然后创建 add-home-description.js:
1 2 3
| hexo.bak/ └── scripts/ └── add-home-description.js ← 新建的注入脚本
|
Hexo 会自动加载 scripts/ 目录下的所有 JS 文件,这些文件在 Hexo 启动时执行,可以使用全局 hexo 对象注册注入器。
3.2 注入 CSS 样式
使用 hexo.extend.injector.register() 注册注入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| hexo.extend.injector.register('head_end', ` <style> .home-banner-description { margin-top: 1rem; font-size: 1.1rem; color: rgba(255, 255, 255, 0.85); letter-spacing: 0.05em; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); opacity: 0; transform: translateY(20px); transition: opacity 0.8s ease-out 0.3s, transform 0.8s ease-out 0.3s; } .home-banner-description.is-visible { opacity: 1; transform: translateY(0); } [data-user-color-scheme="dark"] .home-banner-description { color: rgba(208, 208, 208, 0.85); } </style> `, 'home');
|
参数说明:
- 第一个参数
'head_end':注入到 </head> 前
- 第二个参数:要注入的 HTML 字符串
- 第三个参数
'home':仅在首页生效(可选值:home, post, page, archive, category, tag, default 等)
3.3 注入 JavaScript
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
| hexo.extend.injector.register('body_end', ` <script> (function() { var subtitle = document.getElementById('subtitle'); if (!subtitle) return;
// 从 hexo.config.description 获取描述文字 var description = '${hexo.config.description}'; if (!description) return;
// 创建 description 元素 var descDiv = document.createElement('div'); descDiv.className = 'home-banner-description'; descDiv.textContent = description;
// 插入到 subtitle 后面 subtitle.parentNode.insertBefore(descDiv, subtitle.nextSibling);
// 触发过渡动画 requestAnimationFrame(function() { descDiv.classList.add('is-visible'); }); })(); </script> `, 'home');
|
核心逻辑:
- 找到
#subtitle 元素
- 读取
hexo.config.description 作为描述文字
- 创建
<div class="home-banner-description"> 元素
- 插入到 subtitle 的下一个兄弟节点位置
- 通过添加
is-visible class 触发 CSS 过渡动画
四、踩坑记录
4.1 坑一:Typed.js 游标的位置
问题:实现后发现打字机的游标跑到了 description 的下方,而不是紧跟在 subtitle 后面。
原因分析:Typed.js 会把游标 <span class="typed-cursor"> 作为兄弟节点追加到 #subtitle 后面,而不是放在 subtitle 内部。
所以原来的 DOM 结构是:
1 2
| <span id="subtitle">Carl Victorの小窝</span> <span class="typed-cursor">_</span>
|
我的代码用 subtitle.nextSibling 作为插入参照,结果 description 插到了 subtitle 和游标之间:
1 2 3
| <span id="subtitle">Carl Victorの小窝</span> <div class="home-banner-description">...</div> ← 插在这里了 <span class="typed-cursor">_</span> ← 游标跑到了下面
|
修复方案:先查找 .typed-cursor 元素,如果存在就以游标为插入参照:
1 2 3 4
| var cursor = subtitle.parentNode.querySelector('.typed-cursor'); var insertAfter = cursor || subtitle; insertAfter.parentNode.insertBefore(descDiv, insertAfter.nextSibling);
|
修复后正确的 DOM 顺序:
1 2 3
| <span id="subtitle">Carl Victorの小窝</span> <span class="typed-cursor">_</span> ← 游标紧跟 subtitle <div class="home-banner-description">...</div> ← description 在最下面
|
4.2 坑二:CSS 动画不生效
问题:最初使用 @keyframes fadeInUp 定义动画,但元素始终透明不可见。
原因分析:通过浏览器开发者工具检查,发现 @keyframes 规则没有被正确注入到 <style> 标签中。具体原因是模板字符串的渲染方式导致关键帧语法被意外处理了。
修复方案:改用 CSS transition + JS 添加 class 的方式实现动画:
1 2 3 4 5 6 7 8 9 10 11 12
| .home-banner-description { opacity: 0; transform: translateY(20px); transition: opacity 0.8s ease-out 0.3s, transform 0.8s ease-out 0.3s; }
.home-banner-description.is-visible { opacity: 1; transform: translateY(0); }
|
1 2 3 4
| requestAnimationFrame(function() { descDiv.classList.add('is-visible'); });
|
五、最终完整代码
文件路径:scripts/add-home-description.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
|
hexo.extend.injector.register('head_end', ` <style> .home-banner-description { margin-top: 1rem; font-size: 1.1rem; color: rgba(255, 255, 255, 0.85); letter-spacing: 0.05em; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); opacity: 0; transform: translateY(20px); transition: opacity 0.8s ease-out 0.3s, transform 0.8s ease-out 0.3s; } .home-banner-description.is-visible { opacity: 1; transform: translateY(0); } [data-user-color-scheme="dark"] .home-banner-description { color: rgba(208, 208, 208, 0.85); } </style> `, 'home');
hexo.extend.injector.register('body_end', ` <script> (function() { var subtitle = document.getElementById('subtitle'); if (!subtitle) return;
var description = '${hexo.config.description}'; if (!description) return;
var descDiv = document.createElement('div'); descDiv.className = 'home-banner-description'; descDiv.textContent = description;
// 查找打字机游标元素,确保 description 插入在游标之后 var cursor = subtitle.parentNode.querySelector('.typed-cursor'); var insertAfter = cursor || subtitle; insertAfter.parentNode.insertBefore(descDiv, insertAfter.nextSibling);
// 强制回流后添加 visible class 触发过渡动画 requestAnimationFrame(function() { descDiv.classList.add('is-visible'); }); })(); </script> `, 'home');
|
六、效果展示
最终效果:
- 首页 banner 中,subtitle “Carl Victorの小窝” 下方显示描述文字
- 打字机游标紧跟在 subtitle 末尾闪烁
- 描述文字有淡入上滑的过渡动画
- 暗色模式下自动适配文字颜色
- 仅首页生效,不影响其他页面
七、为什么选择注入方式
相比于直接修改主题源码,使用注入器的方式有以下优势:
| 对比项 |
修改主题源码 |
Hexo 注入器 |
| 主题升级 |
每次升级都要重新改 |
完全不受影响 |
| 版本管理 |
修改 node_modules 无法跟踪 |
scripts/ 目录可纳入 Git |
| 代码组织 |
改动分散在主题各处 |
集中在一个脚本文件 |
| 可复用性 |
只能用于当前主题 |
可迁移到其他 Hexo 主题 |
| 调试难度 |
需要重启 Hexo 并找文件 |
只需看一个文件 |
当然,注入器也有局限性:
- 适合追加内容、样式覆盖、脚本注入
- 如果需要大幅修改模板结构,还是得用 Fluid 的
theme_inject 过滤器或直接 fork 主题
对于”在 subtitle 下方加一行描述”这样的小需求,Hexo 注入器 + JS 的方式是最轻量、最优雅的方案。
八、参考资料