自定义
本页讲解如何给博客加自己的东西:自定义 JS/CSS、注入点机制、自定义 tag 插件,以及主题配置的覆盖方式。改代码属于"开发级"操作,动手前先读「安装与主题配置」篇了解目录结构,改完按「部署」篇选择发布方式。
核心原则:能不改主题源码就不改。优先用 inject_point 注入和配置覆盖,主题升级(子模块
themes/matery/)时才不会被冲突拖累。阅读方式:每节按「如何启用 → 常用选项 → 全部可调项 → 边界与冲突」展开——先看效果(页面上能看到什么、产物里多出什么),再看产生该效果的对应源码/配置,复制即可用。
1. 自定义 JS/CSS
用途:给全站或单页追加脚本与样式,最常见的一类定制。
学习路径:如何启用 → 常用选项 → 全部可调项 → 边界与冲突
1.1 如何启用
效果:只有写了引用的那个页面加载 my-custom.css 的规则、执行 my-custom.js 的代码(例如控制台多打印一行日志);其他页面不受影响。
对应源码(文章/页面正文,路径按实际文件替换):
<!-- 文章/页面正文里直接引用 -->
<link rel="stylesheet" href="/js/my-custom.css">
<script src="/js/my-custom.js" defer></script>输出:文件放 source/js/、source/css/ 等任意目录,构建时原样复制到 public/ 对应路径(即 source/js/my-custom.js → public/js/my-custom.js),页面按上面写的路径就能取到。
1.2 常用选项
① skip_render 目录里的 HTML 保持原样
效果:目录内的文件不经过 Hexo 渲染,原样复制到 public/——适合放独立 HTML 页面;普通 .js/.css 本来就不渲染,不受影响。
对应配置(根 _config.yml 的 skip_render 段,节选):
skip_render:
- 'nav/**'
- 'js/**'
- 'libs/**'
- 'live2d/**'
- 'medias_webp/**'
- 'html-file/**/*.html'② 走 CDN 时仍用相对路径引用
效果:/js/xxx.js 这类以 / 开头的路径指向站点自身域名,不经过 CDN 域;CDN 只作用于主题配置 cdn 段里的主题资源。
对应配置(主题配置 cdn 段,详见「全局配置」篇):
cdn:
url: "{cdnUrl}" # JS/CSS CDN URL
mediaUrl: "{mediaUrl}" # 图片/媒体 CDN URL
resUrl: "{resUrl}" # 资源 CDN URL③ 不想逐页引用 → 用注入点或配置键
效果:脚本/样式在所有页面生效,且不污染主题模板文件。
对应方式:注入点见 §2,custom_js / custom_css 见 §5。
1.3 全部可调项
| 方式 | 生效范围 | 文件放哪 | 是否改主题源码 |
|---|---|---|---|
页面内 <link>/<script> | 单页 | source/js/、source/css/ | 否 |
| inject_point 注入 | 全站 | themes/matery/scripts/filters/ | 是(新增脚本文件) |
custom_js / custom_css 配置 | 全站 | source/js/、source/css/ + 配置模板 | 否(只改配置) |
1.4 边界与冲突
source/下被skip_render覆盖的目录(nav/、docs/之外的静态目录、live2d/等)不会渲染,自定义 HTML 放这些目录时保持原样;普通.js/.css不受影响。- 同一条脚本既用
custom_js又在页面里<script>引用会加载两次,选一种即可。
1.5 小结
- 最小可用:
custom_js/custom_css各填一个路径(§5) - 只要单页:页面内直接
<link>/<script>,不动全局 - 要插到固定位置:用 inject_point(§2)
- 改了不生效:先确认文件已复制到
public/,再按「常见问题」篇清缓存
2. inject_point 注入机制
用途:在不修改主题模板的前提下,向页面的固定位置插入 HTML/JS/CSS。主题从 NexT 继承了一套注入系统,
layout/layout.ejs中实际使用了 3 个视图注入点。学习路径:如何启用 → 常用选项 → 全部可调项 → 边界与冲突
2.1 如何启用
效果:所有页面(首页、文章页、独立页)在 </body> 之前多出你注入的那段内容——例如一个全局脚本标签,或一段统计代码。
对应源码(themes/matery/layout/layout.ejs):
<!-- layout.ejs -->
<%- inject_point('bodyBegin') %> <!-- <body> 开头 -->
<%- inject_point('header') %> <!-- 页头位置 -->
<%- inject_point('bodyEnd') %> <!-- </body> 之前 -->2.2 常用选项
① 注册一段全局脚本(raw)
效果:三个注入点中 bodyEnd 位置原样输出 <script src="/js/my-custom.js" defer></script>,全站每页都带这个标签。
对应源码(新建 themes/matery/scripts/filters/my-injects.js):
// themes/matery/scripts/filters/my-injects.js
'use strict';
const path = require('path');
hexo.extend.filter.register('theme_inject', function(injects) {
// file(name, 文件路径):注册模板文件(name 无扩展名时取文件扩展名,路径相对 hexo 根目录)
injects.bodyEnd.file('my-custom', path.join(hexo.theme_dir, 'layout/_partial/common/my-custom.ejs'));
// raw(name, 内容字符串):直接注册 HTML/JS 内容
injects.bodyEnd.raw('my-analytics', '<script src="/js/my-custom.js" defer></script>');
}, -99); // -99 = 最先执行,与 default-injects.js 保持一致输出:layout/_partial/common/my-custom.ejs 的内容被原样渲染进 <%- inject_point('bodyEnd') %> 所在位置;raw 注册的字符串则直接输出,不经过模板。
② 控制同一点多个注入的顺序
效果:order 越小越先输出;同一点注册多个脚本时,靠它决定谁先执行。
对应源码:
injects.bodyEnd.raw('my-analytics', '<script src="/js/a.js"></script>', {}, {}, 10);injects.<point>.file(name, path, locals, options, order) 第三个参数起依次为 locals/options/order。
③ 改注入位置
效果:把内容从页尾挪到页头(headEnd)或文章元信息上方(postMetaTop),页面上的出现位置随之改变。
对应源码:
injects.headEnd.raw('my-meta', '<meta name="my-flag" content="1">');
injects.postMetaTop.raw('my-badge', '<span class="chip">已校对</span>');注入点实际调用位置(12 个点均已在模板中调用):
headEnd(head.ejs)、footer(footer.ejs)bodyBegin/header/bodyEnd(layout.ejs)postMetaTop/postMarkdownBegin/postMarkdownEnd/postCopyright/postComments(post-detail.ejs)pageComments(bb/msg)、linksComments(friends)
postMetaBottom/postLeft/postRight已从注入点定义中移除(非「已定义未调用」;评论页用inject_point('pageComments') || partial('_partial/comments')做回退)。
④ 正文内的注入点
效果:内容出现在文章正文块的开头/结尾(postMarkdownBegin / postMarkdownEnd),随文章滚动而不是贴在页头页尾。
对应源码:
injects.postMarkdownBegin.raw('my-before', '<div class="notice">正文开始</div>');
injects.postMarkdownEnd.raw('my-after', '<div class="notice">正文结束</div>');2.3 全部可调项
| 项 | 类型 | 默认 | 可选值/范围 | 作用 |
|---|---|---|---|---|
injects.<point>.raw(name, html, locals?, options?, order?) | 方法 | — | 任意 HTML 字符串 | 直接注册内容 |
injects.<point>.file(name, path, locals?, options?, order?) | 方法 | — | 模板文件绝对路径 | 注册模板文件 |
order | number | 注册顺序 | 越小越先输出 | 同一点多个注入的排序 |
theme_inject filter 优先级 | number | — | 如 -99 | 决定注册回调执行先后(与 default-injects.js 一致) |
2.4 边界与冲突
- 注入内容不是从主题配置加载的 HTML 字符串,而是由 hexo 脚本通过
theme_injectfilter 注册到运行时注册表——scripts/events/lib/injects.js在生成前执行hexo.execFilterSync('theme_inject', injects),把结果写入theme.config.injects;inject_pointhelper(scripts/helpers/engine.js)渲染时读取该注册表并输出。 - 样式注入点(
variable/mixin/style)是另一套机制:注册的是.styl文件路径,由 Stylus 编译期注入,与视图注入点不同,不要混用。 - 注入内容原样输出,写
<script>时建议带defer(见「性能优化」篇 LCP 分层原则)。
2.5 小结
- 最小可用:一个
injects.bodyEnd.raw(...)就够了 - 要复用模板:用
.file(...)指向_partial/下的 EJS - 位置不对:确认注入点名字拼写,并查 §2.2 的调用位置清单
- 样式注入:那是 Stylus 的
style注入点,别和视图注入点混用
3. 自定义 tag 插件
用途:在 Markdown 里用短代码生成复杂 HTML。主题内置 14 个 tag 插件(note/tabs/timeline/mermaid/pdf/github-card 等,用法见「Tag 插件」篇),不够用时可自己写。
学习路径:如何启用 → 常用选项 → 全部可调项 → 边界与冲突
3.1 如何启用
效果:文章里的 {% mybox 注意事项 %} 渲染成一个带标题的卡片(<div class="my-box">),卡片正文里的 Markdown 正常渲染(**加粗** 变粗体),而不是显示原始短代码。
对应源码(新建 themes/matery/scripts/tags/mybox.js):
// themes/matery/scripts/tags/mybox.js
'use strict';
function mybox(args, content) {
const title = args.join(' ') || '提示';
return `<div class="my-box"><strong>${title}</strong><div class="my-box-body">${hexo.render.renderSync({ text: content, engine: 'markdown' })}</div></div>`;
}
hexo.extend.tag.register('mybox', mybox, { ends: true });用法(文章正文):
{% mybox 注意事项 %}
这里写**任意 Markdown**,会被渲染为卡片内容。
{% endmybox %}输出:构建产物 HTML 里出现 <div class="my-box"><strong>注意事项</strong><div class="my-box-body"><p>这里写<strong>任意 Markdown</strong>…</p></div></div>。
3.2 常用选项
① 行内 tag(无闭合)
效果:只输出一段 HTML,不需要 {% endmybox %};适合徽章、图标这类单行内容。
对应源码:
function mybadge(args) {
return `<span class="my-badge">${args[0] || 'NEW'}</span>`;
}
hexo.extend.tag.register('mybadge', mybadge); // 不带 ends 即行内输出:文章里 {% mybadge 热门 %} → <span class="my-badge">热门</span>。
② 不渲染 Markdown,只取原始文本
效果:content 里的 ** 等符号原样保留,不再变成 <strong>。
对应源码:
function rawbox(args, content) {
return `<pre class="raw-box">${content}</pre>`;
}
hexo.extend.tag.register('rawbox', rawbox, { ends: true });③ 插件名与文件名
效果:文件名即插件名——mybox.js 注册的 mybox,文章里写的就是 {% mybox %};改文件名等于改短代码名。
对应源码:无配置项,靠 hexo.extend.tag.register('mybox', ...) 的第一个参数。
3.3 全部可调项
| 项 | 类型 | 默认 | 可选值/范围 | 作用 |
|---|---|---|---|---|
register(name, fn) | 方法 | — | 名称字符串 + 函数 | 注册行内 tag |
options.ends | bool | false | true / false | true 时需要 {% endxxx %} 闭合 |
content | string | — | 标签间原始文本 | 块级 tag 的内容(需自行渲染 Markdown) |
args | array | [] | 空格分隔的参数 | 标签后的参数列表 |
3.4 边界与冲突
{ ends: true }表示需要{% endmybox %}闭合标签;漏写结束标签会构建失败(unknown block tag类报错)。content默认是原始文本,需要渲染 Markdown 时用hexo.render.renderSync(注意这是一个 Hexo 内部 API,仅在构建时执行)。- 文件命名即插件名(
mybox.js→{% mybox %}),改完必须hexo clean && hexo generate(见「常见问题」篇缓存不生效)。 - 想全局可用且不动主题源码:把文件放进主题
scripts/是唯一方式(主题是子模块,改动需双仓库提交,见「部署」篇)。
3.5 小结
- 最小可用:一个
hexo.extend.tag.register(...)文件 - 要闭合:
{ ends: true }+{% endxxx %} - 要 Markdown:
content过一遍hexo.render.renderSync - 改完没反应:
hexo clean后再生成
4. 主题配置覆盖(模板注入机制)
用途:主题配置在 CI 构建时被动态生成——
userConfig/_config.tmp.yml是模板,构建时复制为themes/matery/_config.yml并替换占位符。改主题配置必须改模板,直接改themes/matery/_config.yml会被覆盖。学习路径:如何启用 → 常用选项 → 全部可调项 → 边界与冲突
4.1 如何启用
效果:在模板里加/改一个键,跑一次构建后 themes/matery/_config.yml 里出现同样的键,且占位符已被替换成真实 URL/版本号;只改 themes/matery/_config.yml 则在下次构建后被还原。
对应命令:
tools/cicd.sh -d # 容器内:配置注入 + 构建(不推送)
grep -n "myFeature" themes/matery/_config.yml # 验证注入结果输出:grep 能打印出你在 userConfig/_config.tmp.yml 里写的键,说明模板已覆盖生效。
4.2 常用选项
① 模板占位符
效果:模板里的花括号占位符在构建后被替换成带版本号的 CDN 地址;源模板保持占位符不变。
对应配置(tools/cicd.sh 用 sed 替换):
| 占位符 | 替换为 | 示例 |
|---|---|---|
{cdnPathVersion} | jsDelivr 带版本路径 | https://cdn.jsdelivr.net/gh/appotry/hexo@1.1 |
{cdnPathLatest} | latest CDN 路径 | https://cdn.jsdelivr.net/gh/appotry/hexo@latest |
{urlVersion} | 文件版本号 | ?v={urlVersion} |
{cdnUrl} / {mediaUrl} / {resUrl} | 各 CDN 域名 | https://cfblog.17lai.site |
② 自定义配置项的完整链路
效果:新增一个 myFeature 开关后,EJS 模板能读到 theme.myFeature.enable,Stylus 能读到 theme-config("myFeature.text"),页面按配置渲染。
对应配置:
- 模板
userConfig/_config.tmp.yml加键:myFeature: enable: true text: 你好 - Stylus 里读(
themes/matery/source/css/下):$my-feature-color = theme-config("myFeature.text", "默认值") - 模板里读(
layout/下 EJS):<% if (theme.myFeature.enable) { %> <div><%= theme.myFeature.text %></div> <% } %> - 发布:改配置后
tools/cicd.sh -r "特性"递增版本号(配置属于代码侧改动,见「部署」篇发布流程选择)
输出:构建后页面出现 <div>你好</div>;theme-config 的第二个参数是键缺失时的回落值。
4.3 全部可调项
| 项 | 位置 | 作用 |
|---|---|---|
userConfig/_config.tmp.yml | 仓库 | 唯一权威模板(CI 复制到主题配置) |
themes/matery/_config.yml | 主题子模块 | 构建产物,不要手改 |
theme-config("a.b", "默认") | Stylus | 读取配置,键缺失时用默认值 |
theme.a.b | EJS | 直接读配置对象 |
4.4 边界与冲突
tools/cicd.sh构建流水线会注入配置(userConfig/_config.tmp.yml模板 sed 替换占位符)、处理图片 URL(imgurl.sh --weserv2githubpage)、生成 PWA Service Worker(userConfig/sw.tmp.js→source/sw.js),改模板前先grep确认键名没有被构建脚本占用。- 主题配置有 3000+ 行且在 CI 时被整体覆盖,本地
hexo server读的是当前themes/matery/_config.yml——两者不一致时以模板为准。
4.5 小结
- 改配置:只改
userConfig/_config.tmp.yml - 读配置:Stylus 用
theme-config(),EJS 用theme.xxx - 验证:构建后
grep主题配置里是否出现该键 - 发布:配置属代码侧,用
-r
5. 自定义文件配置键(custom_file_path / custom_js / custom_css)
用途:通过主题配置直接指定自定义 JS/CSS 文件路径,全站自动加载——比 §1 方式一(页面内引用)更省事,比 §2 注入点更简单,适合"只想加个脚本/样式"的场景。
学习路径:如何启用 → 常用选项 → 全部可调项 → 边界与冲突
5.1 如何启用
效果:配置里列出的文件在每个页面的 <head>(CSS)和 </body> 前(JS)被自动引入,所有页面生效,无需逐页写标签。
对应配置(主题配置):
# 指定自定义 .js 文件路径,支持列表;路径相对 source 目录
# 如 /js/custom.js 对应存放目录 source/js/custom.js
custom_js:
- /js/custom.js
# 指定自定义 .css 文件路径,用法和 custom_js 相同
custom_css:
- /css/custom.css
# 自定义文件路径(Next 主题注入参考,本项目留空)
custom_file_path:输出:构建后每个页面 HTML 里出现对应 <script src="/js/custom.js"> / <link rel="stylesheet" href="/css/custom.css">,标签由 scripts.ejs / css.ejs 生成。
5.2 常用选项
① 多个文件按顺序加载
效果:列表里越靠前越先加载,后面的文件可以依赖前面的(例如先 jQuery 兼容层、后业务脚本)。
对应配置:
custom_js:
- /js/vendor.js
- /js/app.js② 只加 CSS
效果:只注入样式,不产生额外 JS 请求。
对应配置:
custom_css:
- /css/custom.css③ 保持 custom_file_path 为空
效果:本项目不使用该键;留空时构建不注入额外文件,行为与未配置一致。
对应配置:
custom_file_path:5.3 全部可调项
| 键 | 类型 | 默认 | 可选值/范围 | 作用 |
|---|---|---|---|---|
custom_js | array | 空 | 相对 source/ 的 .js 路径列表 | 全站加载自定义 JS |
custom_css | array | 空 | 相对 source/ 的 .css 路径列表 | 全站加载自定义 CSS |
custom_file_path | 任意 | 空 | 本项目未使用 | Next 主题注入参考,留空即可 |
说明:
- 文件放
source/js/、source/css/等目录,构建时原样复制到public/对应路径 custom_js/custom_css为列表,可配置多个文件,按顺序加载custom_file_path参考自 Next 主题 default-injects.js,本项目未使用,留空即可
5.4 边界与冲突
- 路径以
source/为根(/js/custom.js→source/js/custom.js),写错路径时构建不报错但页面 404。 - 与 §2 注入点注册同一个文件会重复加载;与页面内引用同样会重复。
- 配置键在
userConfig/_config.tmp.yml里,直接改主题_config.yml会被下一次构建覆盖(见 §4)。
5.5 小结
- 最小可用:
custom_css: [/css/custom.css]或custom_js: [/js/custom.js] - 多文件:列表顺序即加载顺序
- 要条件加载/固定位置:改用注入点(§2)
- 404:检查路径是否相对
source/
6. 加密架构(HBE v4)
用途:主题内置加密(2026-08-09 自研,借鉴 hexo-blog-encrypt v4,wire 格式
format: '4'),覆盖三个加密面。学习路径:如何启用 → 常用选项 → 全部可调项 → 边界与冲突
6.1 如何启用
效果:打开被加密的文章时正文不可见,先弹出密码框;输入正确密码后正文解密显示,密码错误时提示错误文案。
对应配置(文章 front-matter + 主题配置):
# 文章 front-matter
password: "你的密码"# 主题配置 encrypt 段
encrypt:
enable: true输出:该篇 HTML 内是 AES-256-GCM 密文(不含明文正文),前端用 WebCrypto 解密后渲染。
6.2 常用选项
三个加密面:
| # | 加密面 | 声明方式 | 实现位置 |
|---|---|---|---|
| a | 整篇文章 | front-matter password,或文章标签命中 encrypt.tags | scripts/encrypt/index.js 的 after_post_render filter(优先级 1000) |
| b | 文章内片段 | Markdown 中 {% encrypt 密码 "提示标题" "内容简介" %}…{% endencrypt %} | scripts/tags/encrypt-tag.js 暂存 + 同一 filter(优先级 2000)二次渲染 |
| c | Wiki / 相册页面 | Wiki 侧栏数据 encrypt_password(见「多语言与 Wiki 系统」篇 §10);相册用 encryptHtml helper | layout/wiki.ejs、gallery.ejs |
① 整篇加密
效果:只有该篇需要密码,其他文章正常打开。
对应配置:front-matter password(见 §6.1)。
② 按标签批量加密
效果:给文章打上指定标签即自动加密,不用逐篇写 password。
对应配置:
encrypt:
tags:
- name: 私密
password: "你的密码"③ 刷新后免输密码(autoSave)
效果:同一浏览器刷新/返回时跳过密码框,直接解密;换浏览器或清 localStorage 后需重新输入。
对应配置:
encrypt:
autoSave: true④ 跨 clean 构建稳定 salt(stableSalt)
效果:同一篇文章重复构建时 salt 不变,已缓存的派生密钥继续有效;关闭时每次构建 salt 都变。
对应配置:
encrypt:
stableSalt: true6.3 全部可调项
| 键 | 说明 |
|---|---|
encrypt.enable | 加密总开关 |
encrypt.abstract | 加密文章列表摘要占位(同时覆盖 excerpt,防内容泄露) |
encrypt.message / encrypt.wrong_pass_message | 提示语 / 密码错误提示 |
encrypt.tags | {name, password} 列表,文章标签命中即加密 |
encrypt.autoSave | 派生密钥写入 localStorage,刷新跳过密码(默认关) |
encrypt.stableSalt | 跨 clean build 稳定 PBKDF2 salt(默认 false) |
encrypt.kdf.iterations | PBKDF2 迭代次数;下限 100000,推荐 ≥ 600000,默认 250000 |
文章 front-matter 可覆盖
password/abstract/message/theme/wrong_pass_message/silent/autoSave/stableSalt/kdf;以模板userConfig/_config.tmp.yml为准。
6.4 边界与冲突
加密机制(服务端 scripts/encrypt/crypto.js 与前端 source/libs/hbe/hbe-bundle.js(WebCrypto)参数必须一致):
- 密钥派生 PBKDF2-SHA256,迭代次数由
encrypt.kdf.iterations决定 - 对称加密 AES-256-GCM;32B 随机 salt + 12B 随机 nonce;密文 =
ciphertext || authTag(16B) stableSalt: true时 salt 由文章 permalink 派生(stableSaltFromPermalink),同文重复构建 salt 稳定、刷新缓存仍有效;nonce 每次仍随机,避免 nonce 复用
多容器 storageKey:加密片段带 data-storage-key 属性;hbe-bundle.js 以 storageKey || location.pathname 作为 localStorage 缓存键(前缀 hbe.v4.),使同一页的多个加密容器互不串键——片段为 segment.<path>#<id>,Wiki 为 wiki.<name>,整篇留空(回退到页面路径)。
搜索引擎排除(三引擎均不索引加密内容):本地搜索 scripts/generators/local-search.js 跳过 post.encrypt === true;Algolia scripts/generators/algolia-search.js 对 record.encrypt === true 返回 null;Pagefind 由 layout/_partial/post/post-detail.ejs 在 page.encrypt 为真时不输出 data-pagefind-body 排除。
6.5 小结
- 最小可用:
encrypt.enable: true+ front-matterpassword - 只藏一段:用
encrypttag,不加密整篇 - 整栏加密:Wiki 侧栏数据里的
encrypt_password - 密码写明文:HBE v4 直接写明文,无需 SHA256 预加密
附:自定义速查表
| 需求 | 方式 | 文件位置 |
|---|---|---|
| 单页脚本/样式 | Markdown 里 <link>/<script> 引用 | source/js/、source/css/ |
| 全站脚本/样式 | inject_point 注入 | themes/matery/scripts/filters/ |
| 新短代码 | tag 插件 | themes/matery/scripts/tags/ |
| 主题配置项 | 模板加键 + Stylus/EJS 读取 | userConfig/_config.tmp.yml |
| 全局 HTML 片段 | 注入点或 _partial/ 模板 | 配置或 themes/matery/layout/_partial/ |
| 加密(HBE v4) | 整篇 / 片段 / Wiki 三面加密 | themes/matery/scripts/encrypt/ |