Hexo注入代码实现Fluid主题首页自定义

一、背景

在使用 Hexo + Fluid 主题搭建博客的过程中,我希望在首页 banner 的 subtitle(副标题)下方添加一行站点描述(description)。

最初的想法很简单——直接修改主题的模板文件,但这样做有几个问题:

  1. 主题升级困难:每次更新主题都需要重新修改源码
  2. 维护成本高:修改点分散,时间久了容易忘记
  3. 版本管理混乱:修改 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/ # EJS 模板文件
│ ├── index.ejs # 首页模板
│ └── _partials/
│ ├── header/
│ │ └── banner.ejs # 横幅模板(subtitle 在这里)
│ └── 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
<!-- banner.ejs 简化结构 -->
<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
// 注入 CSS 样式
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
// 注入 JS 脚本,在 subtitle 下方添加 description
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');

核心逻辑:

  1. 找到 #subtitle 元素
  2. 读取 hexo.config.description 作为描述文字
  3. 创建 <div class="home-banner-description"> 元素
  4. 插入到 subtitle 的下一个兄弟节点位置
  5. 通过添加 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
// 查找打字机游标元素,确保 description 插入在游标之后
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
// JS 中添加 class 触发过渡
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
/**
* 在首页 subtitle 下方注入 description
* 使用 Hexo 原生注入器 + JavaScript 实现
*/

// 注入 CSS 样式
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');

// 注入 JS 脚本,在 subtitle 下方(游标之后)添加 description 并触发动画
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 的方式是最轻量、最优雅的方案。


八、参考资料


Hexo注入代码实现Fluid主题首页自定义
https://blog.carlvictor.cn/posts/f3a7c2d9.html
作者
Carl Victor
发布于
2026年7月28日
许可协议