自定义
本页讲解如何给博客加自己的东西:自定义 JS/CSS、注入点机制、自定义 tag 插件,以及主题配置的覆盖方式。改代码属于"开发级"操作,动手前先读「安装与主题配置」篇了解目录结构,改完按「部署」篇选择发布方式。
核心原则:能不改主题源码就不改。优先用 inject_point 注入和配置覆盖,主题升级(子模块
themes/matery/)时才不会被冲突拖累。
1. 自定义 JS/CSS
用途:给全站或单页追加脚本与样式,最常见的一类定制。
方式一:放 source/ 下,页面里引用(无需改主题源码):
<!-- 文章/页面正文里直接引用 -->
<link rel="stylesheet" href="/js/my-custom.css">
<script src="/js/my-custom.js" defer></script>- 文件放
source/js/、source/css/等任意目录,构建时原样复制到public/对应路径 - 注意
_config.yml的skip_render段:source/下的部分目录(如nav/、docs/、live2d/)会跳过渲染,自定义 HTML 放这些目录时保持原样;普通.js/.css不受影响 - 生产环境走 CDN 时,静态资源 URL 由主题配置
cdn段决定(见「全局配置」篇),自定义文件直接/js/xxx.js引用即可,走站点自身域名
方式二:注入点全局注入(推荐,见下节)——脚本/样式会在所有页面生效,且不污染主题文件。
2. inject_point 注入机制
用途:在不修改主题模板的前提下,向页面的固定位置插入 HTML/JS/CSS。主题从 NexT 继承了一套注入系统,layout/layout.ejs 中实际使用了 3 个视图注入点:
<!-- layout.ejs -->
<%- inject_point('bodyBegin') %> <!-- <body> 开头 -->
<%- inject_point('header') %> <!-- 页头位置 -->
<%- inject_point('bodyEnd') %> <!-- </body> 之前 -->怎么用:注入内容不是从主题配置加载的 HTML 字符串,而是由 hexo 脚本通过 theme_inject filter 注册到运行时注册表——scripts/events/lib/injects.js 在生成前执行 hexo.execFilterSync('theme_inject', injects),把结果写入 theme.config.injects;inject_point helper(scripts/helpers/engine.js)渲染时读取该注册表并输出。
以主题真实写法(scripts/filters/default-injects.js)为模板,自定义注入放在主题 scripts/ 下任意新文件(如 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') %> 所在位置。injects.<point>.file(name, path, locals, options, order) 第三个参数起依次为 locals/options/order,order 控制同一点多个注入的排序。
bodyEnd是最常用的注入点:追加全局脚本、统计代码、悬浮组件- 主题定义了 12 个视图注入点(
headEnd/header/bodyBegin/bodyEnd/footer/postMetaTop/postMarkdownBegin/postMarkdownEnd/postCopyright/postComments/pageComments/linksComments),12 个点均已在模板中调用:headEnd(head.ejs)、footer(footer.ejs)、postMetaTop/postMarkdownBegin/postMarkdownEnd/postCopyright/postComments(post-detail.ejs)、bodyBegin/header/bodyEnd(layout.ejs)、pageComments(bb/contact/msg)、linksComments(friends)。未在模板中调用的仅postMetaBottom/postLeft/postRight(无注入时返回空字符串,评论页用inject_point('pageComments') || partial('_partial/comments')做回退) - 样式注入点(
variable/mixin/style)是另一套机制:注册的是.styl文件路径,由 Stylus 编译期注入,与视图注入点不同,不要混用 - 注入内容原样输出,写
<script>时建议带defer(见「性能优化」篇 LCP 分层原则)
3. 自定义 tag 插件
用途:在 Markdown 里用短代码生成复杂 HTML。主题内置 12 个 tag 插件(note/tabs/timeline/mermaid/pdf/github-card 等,用法见「Tag 插件」篇),不够用时可自己写。
实现:在 themes/matery/scripts/tags/ 下新增文件,导出一个 Hexo tag 函数:
// 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 %}{ ends: true }表示需要{% endmybox %}闭合标签content默认是原始文本,需要渲染 Markdown 时用hexo.render.renderSync(注意这是一个 Hexo 内部 API,仅在构建时执行)- 文件命名即插件名(
mybox.js→{% mybox %}),改完必须hexo clean && hexo generate(见「常见问题」篇缓存不生效) - 想全局可用且不动主题源码:把文件放进主题
scripts/是唯一方式(主题是子模块,改动需双仓库提交,见「部署」篇)
4. 主题配置覆盖(模板注入机制)
用途:主题配置在 CI 构建时被动态生成——userConfig/_config.tmp.yml 是模板,构建时复制为 themes/matery/_config.yml 并替换占位符。改主题配置必须改模板,直接改 themes/matery/_config.yml 会被覆盖。
模板占位符(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 |
自定义配置项的完整链路(以加一个"我的开关"为例):
- 模板
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 "特性"递增版本号(配置属于代码侧改动,见「部署」篇发布流程选择)
注意:tools/cicd.sh 构建流水线会注入配置(userConfig/_config.tmp.yml 模板 sed 替换占位符)、处理图片 URL(imgurl.sh --weserv2githubpage)、生成 PWA Service Worker(userConfig/sw.tmp.js → source/sw.js),改模板前先 grep 确认键名没有被构建脚本占用。
5. 自定义文件配置键(custom_file_path / custom_js / custom_css)
用途:通过主题配置直接指定自定义 JS/CSS 文件路径,全站自动加载——比 §1 方式一(页面内引用)更省事,比 §2 注入点更简单,适合"只想加个脚本/样式"的场景。
配置(主题配置):
# 指定自定义 .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:说明:
- 文件放
source/js/、source/css/等目录,构建时原样复制到public/对应路径 custom_js/custom_css为列表,可配置多个文件,按顺序加载custom_file_path参考自 Next 主题 default-injects.js,本项目未使用,留空即可
6. 加密架构(HBE v4)
用途:主题内置加密(2026-08-09 自研,借鉴 hexo-blog-encrypt v4,wire 格式 format: '4'),覆盖三个加密面:
| # | 加密面 | 声明方式 | 实现位置 |
|---|---|---|---|
| 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 |
加密机制(服务端 scripts/encrypt/crypto.js 与前端 source/js/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 复用
配置键(主题 encrypt.*,以模板 userConfig/_config.tmp.yml 为准;文章 front-matter 可覆盖 password/abstract/message/theme/wrong_pass_message/silent/autoSave/stableSalt/kdf):
| 键 | 说明 |
|---|---|
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 |
多容器 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 排除。
附:自定义速查表
| 需求 | 方式 | 文件位置 |
|---|---|---|
| 单页脚本/样式 | 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/ |