加载中...

加载中...

自定义

本页讲解如何给博客加自己的东西:自定义 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?)方法—模板文件绝对路径注册模板文件
ordernumber注册顺序越小越先输出同一点多个注入的排序
theme_inject filter 优先级number—如 -99决定注册回调执行先后(与 default-injects.js 一致)

2.4 边界与冲突

  • 注入内容不是从主题配置加载的 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)渲染时读取该注册表并输出。
  • 样式注入点(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.endsboolfalsetrue / falsetrue 时需要 {% endxxx %} 闭合
contentstring—标签间原始文本块级 tag 的内容(需自行渲染 Markdown)
argsarray[]空格分隔的参数标签后的参数列表

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"),页面按配置渲染。

对应配置:

  1. 模板 userConfig/_config.tmp.yml 加键:
    myFeature:
      enable: true
      text: 你好
  2. Stylus 里读(themes/matery/source/css/ 下):
    $my-feature-color = theme-config("myFeature.text", "默认值")
  3. 模板里读(layout/ 下 EJS):
    <% if (theme.myFeature.enable) { %> <div><%= theme.myFeature.text %></div> <% } %>
  4. 发布:改配置后 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.bEJS直接读配置对象

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_jsarray空相对 source/ 的 .js 路径列表全站加载自定义 JS
custom_cssarray空相对 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.tagsscripts/encrypt/index.js 的 after_post_render filter(优先级 1000)
b文章内片段Markdown 中 {% encrypt 密码 "提示标题" "内容简介" %}…{% endencrypt %}scripts/tags/encrypt-tag.js 暂存 + 同一 filter(优先级 2000)二次渲染
cWiki / 相册页面Wiki 侧栏数据 encrypt_password(见「多语言与 Wiki 系统」篇 §10);相册用 encryptHtml helperlayout/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: true

6.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.iterationsPBKDF2 迭代次数;下限 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-matter password
  • 只藏一段:用 encrypt tag,不加密整篇
  • 整栏加密: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/
评论
数据加载中 ...