加载中...

加载中...

本文既是 matery 主题布局与页面功能的完整使用教程,也是 自动化测试靶场。覆盖:文章布局特性、侧边栏组件、响应式断点、相册、搜索、评论、暗色模式、加密、RSS/站点地图、PWA。自动化测试(tools/tests/release-test.sh)会断言下方标注的渲染标记。

阅读方式:每个功能都先看 效果(真实渲染或行为描述),再看 对应配置(产出该效果的最小片段),需要查参数时直接跳到该节的「全部可调项」表。配置片段来自主题配置 userConfig/_config.tmp.yml(构建时注入 themes/matery/_config.yml)。


1. 文章布局特性

1.1 上下篇导航(上一篇/下一篇)

效果:文章末尾左右两张卡片,各显示相邻文章的封面 + 标题 + 摘要,hover 有渐变背景;某一侧没有文章时渲染对方卡片占位(left-badge / right-badge 标记)。

对应配置:没有开关——post-detail.ejs:171 无条件渲染 _partial/post/prev-next.ejs。

# 配置文件里存在 post.prev_next.enable: true,但模板未读取该键,
# 改名/关闭都不会让导航消失。真正能影响它的是 front-matter:
#   hide: true   → prev_post()/next_post() 递归跳过该文,导航不指向它

⚠️ 历史误记:旧版本本文写的 prenext.enable 不是配置键,prenext-posts 只是渲染出来的 DOM id。

测试断言:文章页含 #prenext-posts 容器。

1.2 版权声明

效果:文章底部授权卡片——标题、作者、原文链接、发布日期、CC 图标 + 协议名(如 CC BY-NC-SA 4.0);开了 update_date 还会多一行"更新于"。

对应配置:

post:
  copyright:
    enable: true            # 总开关
    license: 'cc_by_nc_sa'  # 默认协议
    author:
      enable: true          # 显示作者
    post_date:
      enable: true          # 显示发布日期
      format: "LL"
    update_date:
      enable: false         # 显示更新日期
      format: "LL"

常用选项:

① 换协议(8 种 + 两种特殊)

效果:卡片右下角的 CC 图标与名称随之变化;noreprint 显示"不允许转载",pay 显示"付费转载"。

对应配置(license 可选值,映射表见 copyright.ejs):

post:
  copyright:
    license: 'cc_by_nc_sa'
    # cc_by | cc_by_nd | cc_by_sa | cc_by_nc | cc_by_nc_nd | cc_by_nc_sa
    # cc0 | noreprint | pay

② 单篇关闭 / 单篇换协议

效果:copyright: false 让这一篇不出授权卡片;reprintPolicy 只改这一篇的协议,其余文章仍用全局 license。

对应配置(写在文章 front-matter):

---
copyright: false          # 本篇不显示版权卡片
# 或
reprintPolicy: cc_by      # 本篇单独用 CC BY
---

测试断言:文章页含版权声明容器(#reprint-statement / .license-box)。

1.3 文章时效提示

效果:文章开头插入一条 note:发布超过 warning_day 天用 warning 样式,超过 error_day 天升级为 danger/错误样式。本站当前配置为关闭,所以本文看不到这条提示。

对应配置:

post:
  outdate:
    enable: false      # 本站当前关闭;改 true 即对全站文章生效
    warning_day: 365   # 超过 365 天 → warning 样式
    error_day: 3650    # 超过 3650 天 → error 样式

常用选项:

① 只留"过期"不升级

效果:所有过期文章都停在 warning 样式,不会突然变成刺眼的红色块。

对应配置:把 error_day 设得比站龄还大(如 36500)。

② 单篇豁免

效果:常青文章(如本系列教程)不显示过期提示。

对应配置:front-matter 写 outdate: false(该篇不插提示条)。

测试断言:启用后文章页含时效提示元素(outdate 相关 class);当前配置下不断言存在。

1.4 TOC 目录

效果:桌面端右侧悬浮目录树,随滚动高亮当前标题;移动端折叠进侧边栏。

对应配置(主题 _config.yml 顶层 toc,post / wiki / msg / bb 等所有含 TOC 的页面共用):

toc:
  enable: true
  headingSelector: "h2,h3,h4,h5,h6"   # 参与目录的标题级别
  collapseDepth: 0                    # 折叠深度(0 = 全部折叠)

常用选项:

① 只收 h2/h3

效果:目录只两层,侧栏更短,长文滚动时高亮跳变更少(h4 以下不再打断)。

对应配置:

toc:
  headingSelector: "h2,h3"

② 默认展开层级

效果:collapseDepth: 1 时一级标题默认展开其子级,读者不用逐层点开。

对应配置:

toc:
  collapseDepth: 1   # 大于 0:存在下级标题则默认展开

2026-09-25 更新:配置由 post.toc 提升为顶层 toc,wiki 页与 post 页共用同一份配置(此前 wiki 硬编码 h1, h2, h3 且完全忽略配置);同时移除了语义误导的 showToggleBtn(它只决定按钮点击处理器是否注册,并非显隐开关)与无实现的 placement(死配置)。

测试断言:文章页含 #toc-content 目录容器。

1.5 广告位(TOC 下方)

效果:文章 TOC 下方出现一组彩色广告卡片(.tgCard),单条宽或两列(colspan 决定);没有任何一条 enable: true 时整块不渲染(不是空壳)。

对应配置:

post:
  advertisements:
    - id: "ad-1"
      text: "广告文案"
      link: "https://example.com"
      enable: true

字段全解见 §2.10(位置在此,全参数表在那里)。

测试断言:文章页含 .tgCard 广告容器。

1.6 文章字数与阅读时长

效果:文章头部 article-info 显示"字数 · 阅读时长"(如 4.2k 字 · 14 分钟);页脚显示全站总字数。

对应配置(显示开关在 postInfo,计算系数在 post.min2read):

postInfo:
  wordCount: true     # 显示单篇字数
  min2read: true      # 显示阅读时长

post:
  min2read:
    awl: 1            # 每个字词的长度:中文 1 字 = 1 字符 → 1
    wpm: 300          # 每分钟阅读字数:中文技术文章建议 250-350

实现(themes/matery/scripts/helpers/wordcount.js):用三个内置 helper 取代原 hexo-wordcount 插件,函数名与签名完全一致,模板零改动:

helper作用
wordcount(page)单篇字数(中文字符数 + 英文词数)
min2read(page)阅读时长(分钟)= floor(字数 / (awl × wpm)) + 1
totalcount(site)全站总字数(文章 + wiki 页;多语言版本去重;加密文章按明文 post.origin 计数)

常用选项:

① 只显示字数、不显示时长

效果:article-info 少一项,头部更紧凑。

对应配置:

postInfo:
  wordCount: true
  min2read: false

② 按英文阅读速度算

效果:awl: 2 / wpm: 60(等效 120 字/分钟)下 520 字文章显示 5 分钟——这就是旧的错误默认值。

对应配置:

post:
  min2read:
    awl: 2
    wpm: 60

⚠️ 历史坑(2026-09-22 修正):旧默认值 awl: 2 / wpm: 60 等效 120 字/分钟,为英文设计——520 字的中文文章显示 5 分钟;新默认值 awl: 1 / wpm: 300 下同一篇文章显示 2 分钟。

测试断言:文章页含 article-info 字数/时长标记;三个 helper 的行为由 tools/tests/unit/helpers.test.js 覆盖。

1.7 更新时间(更新于)

效果:文章头部显示「更新于 2026年9月26日 上午」(各篇不同);从未改动过的文章不显示该行 —— 由 compare_date(page.date, page.updated) 守卫:两个时间相等时不出这一行。

数据来源(2026-09-26 修复):取文章源文件在 git 中的最后提交时间 —— 既不是文件 mtime,也不等于发布时间。

候选来源为何不用
文件 mtime会被 git checkout/clone/批量脚本重置(实测 142 篇 _posts 的 mtime 全落在同一秒)→ 全站显示同一天
date(发布时间)无法反映修改,出现「更新于 == 发布于」
git 提交时间 ✅提交时固化,不受工作树任何操作影响

对应配置:

postInfo:
  update: true        # 显示"更新于"行的开关(模板读的是这个键)

post:
  updated:
    enable: false     # 兼容位:post-detail 未读它,改它不生效
    date_format: "LL a"   # 日期格式
    relative: false       # true → 显示"3 天前"
    note_class: info      # note 样式:default|primary|info|success|warning|danger|light

updated_option: 'date'    # 仅作兜底——'empty' 会让第三方 sitemap 模板的 post.updated.toDate() 崩溃

常用选项:

① 相对时间

效果:「更新于 3 天前」而非「更新于 2026年9月26日 上午」。

对应配置:

post:
  updated:
    relative: true

② 作者手动指定

效果:该篇显示 front-matter 写的日期,不再用 git 时间。

对应配置(front-matter):

---
updated: 2026-10-01 09:00:00
---

实现:scripts/filters/post-updated.js 在 before_generate(早于全部 generator)用 scripts/utils/git-time.js 读到的 git 时间覆盖 post.updated;作者在 front-matter 写 updated: 时以作者值为准。

测试断言:tools/tests/unit/unit-updated.test.js(4 用例:中文路径命中 / 不存在返回 null / 未显式指定被覆盖且为 moment / 作者显式值保留);文章页含 datetime="…" update。


2. 侧边栏组件(widgets)

本节 10 个组件分两类:页面级自动渲染(无配置键,页面存在即有)与配置驱动(主题 _config.yml 里的键)。每项都标出它由谁渲染、哪个键控制。

2.1 分类雷达图

效果:/categories/ 页面顶部一张 canvas 雷达图,每个分类一个维度,轴长 ∝ 该分类文章数;明暗切换时重绘。

对应配置:无配置键——categories.ejs:11 无条件渲染 _partial/widget/category-radar.ejs,数据来自文章分类。要它出现只需有分类页:

---
title: categories
type: "categories"
layout: "categories"
---

菜单入口是另一回事(menu.<任意名> 的 url + icon),与组件是否渲染无关。

测试断言:/categories/ 含 <canvas> 元素。

2.2 标签云

效果:/tags/ 页面下方一排彩色圆角 chip,标签字号随机(视觉层次),点击跳到对应标签页。

对应配置:无配置键——tags.ejs:13 渲染 tag-cloud.ejs,数据来自全部文章标签。

测试断言:/tags/ 含 .tag-chips .chip。

2.3 词云

效果:/tags/ 页面 ECharts 词云(2026-09-05 自写螺旋 → echarts-wordcloud@2,修复 30s 超时);字号连续 sizeRange [14,56] ∝ 文章数;容器高度自动 max(minHeight, 词数 × heightPerWord) —— 词多不会裁剪,词少不留大片空白。

对应配置:

wordcloud:
  minHeight: 500         # 词云最小高度(px),词少时兜底
  heightPerWord: 2.2     # 每词高度增量(px/词),词多时扩展高度防裁剪

常用选项:

① 词多时更高

效果:500 个标签时容器高度 = max(500, 500×2.2) = 1100px,不会把词挤成一团。

对应配置:

wordcloud:
  heightPerWord: 3      # 每词 3px,词多时容器更高

标题 标签词云 走 i18n;明暗走 Matery.echarts.init 自动切换(无需额外配置)。

测试断言:/tags/ 含 #tag-wordcloud 容器 + echarts 已加载且仅 1 份 + 明暗切换 dispose→reinit(tools/tests/wordcloud.test.js L2g)。

2.4 归档时间线

效果:/archives/ 页面按年/月垂直时间线排列全部文章,圆点 + 年份折叠,滚动淡入。

对应配置:无配置键——archive.ejs 内置渲染(#cd-timeline),文章列表直接来自 site.posts。

测试断言:/archives/ 含 #cd-timeline。

2.5 关于页统计(postCharts)

效果:/about/ 页面三张 ECharts 图:文章发布趋势(#posts-chart)、分类分布(#categories-chart)、标签分布(#tags-chart),明暗自适应。

对应配置:无配置键——about.ejs:48 无条件渲染 post-charts.ejs;需要有 /about/ 页面(layout: about)。

测试断言:/about/ 含 #postCharts 容器 + 多个 <canvas> 元素。

2.6 ArtiTalk 说说

效果:/about/ 页面底部嵌入 ArtiTalk 说说墙(LeanCloud 后端),可发短动态。默认禁用,所以本站关于页看不到它。

对应配置:

artitalk:
  enable: false    # 默认禁用;改 true 才渲染
  appId:           # LeanCloud App ID
  appKey:          # LeanCloud App Key

常用选项:

① 启用说说墙

效果:#artitalk_main 容器出现并加载 jsDelivr 上的 artitalk 脚本,填入 appId/appKey 后即可读写真说。

对应配置:

artitalk:
  enable: true
  appId: "{{LEANCLOUD_APP_ID}}"
  appKey: "{{LEANCLOUD_APP_KEY}}"

模板无 enable 时会整块跳过(about.ejs:62 的 if 守卫),因此默认状态下不断言存在。

测试断言:默认状态下不渲染。

2.7 我的项目(myProjects)

效果:/about/ 页面三列项目卡片,每张含 Font Awesome 图标 + 渐变底 + 项目名 + 一句话简介,点击跳外链。

对应配置:

myProjects:
  enable: true
  data:
    LinuxPTTools:                                  # 项目名(卡片标题)
      icon: fab fa-linux                           # Font Awesome 类名
      iconBackground: 'linear-gradient(to bottom right, #66BB6A 0%, #81C784 100%)'
      url: https://github.com/appotry/PTtool       # 点击跳转
      desc: 为PT用户设计,方便同时刮削和做种。        # 一句话简介

常用选项:

① 关掉整个板块

效果:关于页不渲染 .my-projects 卡片区(其余板块不受影响)。

对应配置:

myProjects:
  enable: false

② 加一个项目

效果:多一张卡片,图标/渐变/链接/简介各按自己的值。

对应配置:在 data: 下追加一个同名层级(键名即标题,可含冒号,如 Docker:hexo)。

测试断言:/about/ 含 .my-projects 容器。

2.8 我的技能(mySkills)

效果:/about/ 页面两列技能条,彩色渐变填充 + 百分比数字,按 percent 宽度渲染。

对应配置:

mySkills:
  enable: true
  data:
    C语言:
      background: 'linear-gradient(to right, #2196F3 0%, #42A5F5 100%)'
      percent: 90%

常用选项:

① 调熟练度

效果:进度条填充宽度随 percent 变化;90% 与 45% 并排时长度明显不同。

对应配置:

mySkills:
  data:
    C语言:
      percent: 45%

② 换配色分组

效果:同一色系的技能在视觉上归为一组(本站按蓝/粉/紫/青/绿五系分组)。

对应配置:同组技能用同一个 background 值即可。

测试断言:/about/ 含 .my-skills 容器。

2.9 归档日历(postCalendar)

效果:/archives/ 页面顶部一块日历热力图(#post-calendar),格子颜色深浅表示当月发文数。

对应配置(注意:这是布尔根键,不是 map):

postCalendar: true    # 在归档页显示文章日历控件

常用选项:

① 关掉日历

效果:归档页直接从时间线开始,没有日历卡片。

对应配置:

postCalendar: false

测试断言:/archives/ 含 #post-calendar 容器。

2.10 侧边栏广告(advertisement)

效果:文章 TOC 下方一组广告卡片;有 image 时显示图片,没有则显示 text;colspan: 2 占整行,colspan: 1 占半行;暗色模式自动换 background_dark。

对应配置:

post:
  advertisements:
    - id: "ad-1"                          # 唯一标识(data-tg-id)
      text: "广告文案"                     # 无 image 时显示的文字
      link: "https://example.com"          # 点击跳转
      background: "#ff6666"                # 亮色底
      background_dark: "#8b3a3a"           # 暗色底(缺省回退亮色)
      colspan: 1                           # 1=半行,2=整行
      enable: true                         # 只有 true 的才渲染

常用选项:

① 图片广告

效果:卡片里是一张图(图片替代文字),文字仅作 alt。

对应配置:

post:
  advertisements:
    - id: "ad-aliHongBao"
      text: "支付宝扫码领红包"
      link: "/msg/"
      background: "var(--card-bg-color)"
      colspan: 1
      image: "/medias_webp/misc/hongbao.webp"
      enable: true

② 整行大卡 + 关掉某条

效果:colspan: 2 的广告独占一行;enable: false 的那条既不渲染也不占位。

对应配置:

post:
  advertisements:
    - id: "ad-3"
      text: "广告招租"
      link: "mailto:admin@example.com"
      background: "#66ff66"
      background_dark: "#3a8b3a"
      colspan: 2
      enable: false

无启用广告时输出空(advertisement.ejs 先 some(ad => ad.enable) 判定)。

测试断言:文章页含 .tgCard 广告容器。

2.11 关于页相册(myGallery)

效果:/about/ 页面下方一排缩略图卡片(/medias_webp/featureimages/ 系列),点击进灯箱。

对应配置:

myGallery:
  enable: true
  data:
    - /medias_webp/featureimages/0.webp
    - /medias_webp/featureimages/1.webp
    - /medias_webp/featureimages/2.webp

常用选项:

① 换图片集合

效果:卡片数量与图片一一对应,顺序即显示顺序。

对应配置:data: 下逐行追加/删除路径即可,无数量上限(多了会换行)。

与 §7 的 /galleries/ 独立相册页不是同一套:这里只是关于页的图片墙,没有封面/描述/加密。

测试断言:/about/ 含 .my-gallery 容器。


3. 响应式断点

用途:用四个 Stylus 变量把视口宽度切成四段,决定导航折叠、侧栏显隐、TOC 位置、特效开关。

学习路径:改哪个变量 → 常见改法 → 全参数表 → 边界与冲突

3.1 如何启用

效果:默认四段——<768px 导航折叠为汉堡菜单、正文单列;768–992px 平板布局、侧边栏收起;>992px 桌面三栏(正文+侧边栏+TOC);>1400px 启用樱花/水波等特效。

对应配置(themes/matery/source/css/_variables/base.styl):

$breakpoint-sm: 368px   // 超小屏(手机竖屏)
$breakpoint-md: 768px   // 平板/手机横屏
$breakpoint-lg: 992px   // 桌面
$breakpoint-fun: 1400px // 特效启用阈值(大屏)

3.2 常用选项

① 提前进入桌面三栏

效果:横向 1280px 的笔记本上就显示侧栏 + TOC,不必等 1400px 宽屏。

对应配置:

$breakpoint-lg: 1200px

② 推迟特效

效果:1400px 以下一律不加载樱花/水波动画,低配机器滚动更顺。

对应配置:

$breakpoint-fun: 1600px

3.3 全部可调项

键类型默认可选值/范围作用
$breakpoint-smlength368px任意 CSS 长度超小屏(手机竖屏)
$breakpoint-mdlength768px任意 CSS 长度平板/手机横屏
$breakpoint-lglength992px任意 CSS 长度桌面三栏
$breakpoint-funlength1400px任意 CSS 长度特效启用阈值(大屏)

3.4 边界与冲突

  • 定义在 Stylus 变量文件而非 _config.yml:改完必须重新编译(clean 构建),只看源码不生效。
  • 四个值共同把宽度切成四段,改任一值会连带移动相邻两段的边界(下面的区间表是结果,不是独立配置)。
  • 与 §4 的内容宽度阶梯正交:断点控制布局形态,max-width 控制列宽上限。

3.5 小结

断点布局表现
<768px导航折叠为汉堡菜单,正文单列
768–992px平板布局,侧边栏收起
>992px桌面三栏(正文+侧边栏+TOC)
>1400px特效启用(樱花/水波等)

效果汇总:Bootstrap/Materialize 栅格自适应,侧边栏在窄屏自动隐藏,TOC 移动端折叠。

测试断言:<768px 导航折叠;>992px 桌面三栏;>1400px 特效启用。


4. 内容宽度统一(2026-09-07 新增)

用途:让正文列在不同屏幕上既铺满又不至于宽到读不下去。

学习路径:为什么要"比例+上限" → 单栏/多栏怎么选 → 宽度阶梯表 → 边界

4.1 如何启用

效果:4K 屏上正文列封顶 1525px(单栏)/ 2250px(多栏),不会拉成 3456px 宽的一行字;FHD 上则是 1125px / 1250px。

对应配置(_variables/base.styl,mixin content-container(wide)):

// 单栏:内容无侧栏
.content-container()      // → 标准宽度阶梯

// 多栏:内容 + 侧栏/TOC
content-container(true)   // → 宽宽度阶梯

设计原则:“比例 + 上限”——内容区 width: 90%(相对屏幕)+ max-width 阶梯(可读性上限)。不要纯 90%——4K 下 90% = 3456px 不可读。

4.2 常用选项

① 单栏页面用标准宽度

效果:wiki 单栏页在 FHD 上 1125px、2K 上 1525px、4K 仍 1525px(封顶)。

对应配置:

.content-container()      // 不传参 = 单栏

② 多栏页面用宽宽度

效果:wiki(多栏)4K 内容列 899px → 1500px(+67%),与 post-container 同宽阶梯。

对应配置:

.content-container(true)  // 传 true = 多栏

选型规则:

  • 单栏(内容无侧栏):content-container() → 标准宽度
  • 多栏(内容 + 侧栏/TOC):content-container(true) → 宽宽度
  • 禁止 layout 手写 max-width(统一走 mixin)

4.3 全部可调项

断点标准(单栏)宽(多栏)
FHD 993-1920px1125px1250px
2K 1921-2560px1525px1750px
4K 2561px+1525px(封顶)2250px

4.4 边界与冲突

  • 阶梯值写死在 mixin 里,改一处影响所有调用方;给单个页面破例会破坏"全站同宽"的视觉一致性。
  • 与 §3 断点正交:断点决定"几栏",这里决定"每栏多宽"。
  • 4K 单栏封顶 1525px 是有意的——再宽阅读时眼球回扫距离过长。

4.5 小结

  • 默认单栏就够了:content-container() 一行
  • 有侧栏/TOC:content-container(true)
  • 别手写 max-width:布局页面之间会对不齐
  • 验证:tools/tests/content-width.test.js(L2m)覆盖 W1 4K wiki==post-container / W3 FHD / W4 2K / W5 单栏封顶 1525px。

4.6 FA 资源版本戳({urlVersion})

效果:发布新版本后浏览器/CDN 拉到新资源(URL 带 ?v=13.13),旧缓存自动失效;本地预览则没有版本后缀,功能不受影响。

对应配置(userConfig/_config.tmp.yml,static_prefix.libs 段):

static_prefix:
  libs:
    faSolid: /libs/awesome/webfonts/fa-solid-900.woff2{urlVersion}
    matery:
      mycss:
        main: /css/main.css{urlVersion}
    highlight:
      highlight: /css/highlight.css{urlVersion}

机制:资源 URL 带版本戳占位符 {urlVersion},由 tools/cicd.sh 的 sed 在构建时替换为实际版本号(从 tools/.VERSION 读取)。

涉及资源:

资源配置键示例
FA WebFontfaBrands/faRegular/faSolid/libs/awesome/webfonts/fa-solid-900.woff2{urlVersion}
主题 CSSmatery/mycss/main/css/main.css{urlVersion}
高亮 CSShighlight/highlightDark/css/highlight.css{urlVersion}

开发者无需手动操作:版本号由 CI 自动注入。

⚠️ 不要手动修改 themes/matery/_config.yml 中的 {urlVersion} 占位符——该文件是构建产物,CI 时被 userConfig/_config.tmp.yml 覆盖。


5. 多语言与回落机制(2026-09-08 新增,2026-09-10 修订)

用途:一套内容按语言前缀输出多份页面,缺翻译时按固定链路回落,而不是 404。

学习路径:开哪几种语言 → 回落顺序 → 翻译文件怎么放 → 全参数 → 边界

5.1 如何启用

效果:只有 enabled: true 的语言才生成页面、才出现在语言下拉里;de/eo/es/ja/ru 等默认关闭,访问 /de/... 会回落到 en。

对应配置(主题 _config.yml 的 preference 段):

preference:
  default_lang: zh-cn         # 默认语言(无前缀路径)
  fallback_lang: en           # 回落中间层(须在 lang_meta enabled 中)
  lang_meta:
    zh-cn:
      name: 简体中文
      flag: 🇨🇳
      enabled: true
    en:
      name: English
      flag: 🇬🇧
      enabled: true
    de:
      name: Deutsch
      flag: 🇩🇪
      enabled: false          # 关闭 → 不生成页面、不出现在下拉

回落链(三层):

请求语言存在? → 用它
→ 不存在 → fallback_lang 存在? → 回落 fallback_lang
→ 不存在 → default_lang 存在? → 回落 default_lang
→ 不存在 → 任意可用语言

5.2 常用选项

① 只做中英双语

效果:构建产物只有 / 与 /en/ 两套,不再生成 de/eo/es/ja/ru 的空壳页面。

对应配置:保持 default_lang: zh-cn + fallback_lang: en + 两者 enabled: true,其余 false。

② 加一门语言

效果:面板下拉多出一个选项,/{lang}/ 与 /{lang}/page/N/ 出现,缺翻译时仍按回落链取内容。

对应配置:把该语言 enabled 改 true,并在 source/{lang}/ 下补翻译文件。

③ 真实翻译映射(post / wiki)

效果:有真实翻译时生成 /{lang}/posts/{abbrlink}/ 真实内容页;无翻译则回落链生成。

对应配置:翻译文件与默认语言同名,放在语言子目录:

类型翻译源路径front-matter
文章source/{lang}/_posts/{同名}.md保留相同 abbrlink + 新增 lang: en
wikisource/{lang}/docs/{同名}.md同上(wiki 页无 abbrlink,按同名路径映射)

中英关联双模式:有 abbrlink 插件 → 相同 abbrlink 关联;无插件 → 同名文件关联(带文件系统兜底检查)。

注:hexo 原生只认 source/_posts/(en/_posts/ 会被忽略),文章翻译由主题 i18n-post-generator.js 生成。

5.3 全部可调项

键类型默认可选值/范围作用
preference.default_langstringzh-cn语言 code默认语言,路径无前缀
preference.fallback_langstringen语言 code(须 enabled)回落中间层
preference.lang_meta.<code>.namestring—任意字符串下拉/面板显示名
preference.lang_meta.<code>.flagstring—emoji下拉国旗
preference.lang_meta.<code>.enabledboolfalse(示例中 en/zh-cn 为 true)true / false是否生成该语言页 + 是否进下拉

5.4 边界与冲突

各页面类型行为(2026-09-10 修订):

页面类型多语言版提示条
文章(post)✅ 有翻译用翻译,无翻译回落链生成✅ 显示"xx 不存在,显示 yy 语言版本"
wiki 页✅ 同上✅ 同上
聚合页(tag/category/archive)✅ 按 enabled 语言生成❌ 只生成不提示
独立页(about/musics/movies/friends/msg/bb/galleries 等)✅ 生成 /{lang}/{page}/(界面文案走 languages/{lang}.yml i18n)❌ 无
首页✅ /en/、/en/page/N/❌ 无

独立页(含 gallery 子页与 pages 集合中 layout:'post' 的历史页如 dash)由 standalone-page-generator.js 按排除法生成(layout ∉ {wiki, tags, categories, 404} 即生成,新增独立页自动覆盖,无需登记清单)。

客户端语言策略(方案 A 全站跟随,2026-09-10):

场景行为
首访无本地偏好且浏览器语言匹配已启用语言 → 弹窗询问切换;接受则记忆并跳转,拒绝则记住默认语言(不再重复弹)
面板切换语言下拉选择 → 写入本地偏好 + 跳转到对应语言版
页面加载 / 刷新有本地偏好 → 直接按偏好加载对应语言版(head.ejs 内联重定向,早于渲染无闪现);爬虫跳过(SEO);无多语言版路径跳过(防 404)
面板回显语言下拉显示本地偏好(localStorage.wiki_lang)

语言前缀规则(url_for_lang):资源(css/js/libs/medias/images/fonts 等)与静态文件(xml/txt/json 等)不加语言前缀;其余内容路径一律加前缀。豁免规则单一来源 scripts/utils/url-lang-blacklist.js。独立页导航因此带上 /en/ 前缀指向对应语言版。

标题 i18n(page_title_i18n,2026-09-10):<title> 与封面大标题(bg-cover-content.ejs)共用 scripts/helpers/page-title.js 的 page_title_i18n:tags/categories/about 按 title、其余按 layout 映射 i18n key;返回 null 时调用方回退原 title。

2026-09-27 补充(i18n 批):

项变更
404 页改客户端语言检测(单文件),按浏览器语言 / 本地偏好动态选文案;不再按路径静态生成多份(/en/404.html 等作废)
archives 聚合页全语言统一分页——不再按语言各自分页导致页数不一致
加密页语言加密提示语 / 按钮文案随页面语言切换(hbe-bundle 兜底文案走 MATERY_I18N,E7)
运行时文案搜索 / 弹窗 / codeLang 等运行时文案 i18n 化(P0/P1)
内链语言文章内链(url_for_lang)指向对应语言版(E2)

5.5 小结

效果汇总:访问 /de/docs/setup/(de 未启用)→ 内容用 zh-cn 加提示条;浏览器语言为 en 的访客首访时被询问是否切换到英文版。

  • 只开两门语言:enabled 是唯一开关,不用删文件
  • 缺翻译不算错:回落链兜底,页面照样 200
  • 资源不加前缀:url-lang-blacklist.js 是唯一豁免清单
  • 测试:tools/tests/lang-fallback.test.js(L13,回落链 4 级 + 配置兼容/过滤/校验 13 断言);tools/tests/release-test.sh L2x(MATERY_LANG 注入 / 回落页 / 9 个独立页 200);tools/tests/unit-standalone.test.js(L13,22 断言);tools/tests/lang-client-strategy.test.js(L11,7 断言)。

6. Wiki 多语言(docs/api/tutorial 子站,2026-09-10 修订)

用途:Wiki 文档子站(docs/api/tutorial)三栏布局 + 中英双语 + 回落机制,与博客文章共用同一套多语言体系。

学习路径:最小 front-matter → 侧栏与加密 → 全参数 → 边界(样式/宽度陷阱)

6.1 如何启用

效果:写了这两个键的页面渲染成三栏布局(左侧栏 + 正文 + 右 TOC);缺 layout 就退化成普通页面,缺 wiki 就没有侧栏。

对应配置(front-matter 必填):

---
layout: wiki        # 必填,否则不渲染三栏布局
wiki: tutorial      # 必填,子站名(docs/api/tutorial)
lang: en            # 英文页必填(坑 3)
---

目录结构:

语言路径
中文(默认)source/{wiki}/(无前缀)
英文source/en/{wiki}/(/en/ 前缀)

回落机制:无英文版 → /en/... 回落显示中文 + 顶部提示条(“no en version”)+ noindex。

6.2 常用选项

① 配侧栏

效果:左侧出现分组导航(guide 下挂 writing 等子项),条目文案跟随语言切换。

对应配置(source/_data/{wiki}-sidebar.yml):

# source/_data/docs-sidebar.yml
structure:
  docs:
    - key: guide
      children:
        - key: writing
          title: 写作指南
  • 条目 key 英文 snake_case,显示文本走 languages/*.yml 翻译

② 整个 wiki 加密

效果:该 wiki 全部页面渲染前先要密码;输入一次后同 wiki 其他页面自动解密免重输(派生密钥存 localStorage 的 wiki.<wiki名>,TTL 1 天)。

对应配置:

# source/_data/docs-sidebar.yml 顶层
encrypt_password: "{{WIKI_PASSWORD}}"
encrypt_message: "请输入访问密码"
encrypt_placeholder: "密码"
encrypt_button_text: "进入"

encrypt_ 前缀元数据键只存在于数据文件顶层(structure 分区之外),天然不参与侧栏导航渲染。

共 4 个键:encrypt_password(必填)+ encrypt_message / encrypt_placeholder / encrypt_button_text(三者可选,缺省用内置文案)。

③ 内部链接

效果:绝对路径 + .html 的链接在英文页自动变成 /en/tutorial/...,不会掉回中文页。

对应配置:

[topic](/tutorial/guide/writing.html)

④ 段落编号

效果:手写编号的 wiki 页不再出现「1. 1. 概述」。

对应配置(与博客一致):

closeAutoTocNum: true

⑤ 新增 wiki 页流程

效果:新页出现在侧栏,中英各一份,9 语言 key 齐全。

对应配置(5 步):

  1. 写中文 source/{wiki}/xxx.md(front-matter layout: wiki + wiki: <子站>)
  2. 补英文 source/en/{wiki}/xxx.md
  3. 更新 source/_data/{wiki}-sidebar.yml
  4. 9 语言翻译 key(languages/*.yml)
  5. hexo clean 重建

6.3 全部可调项

键类型默认可选值/范围作用
layoutstring—wiki启用三栏 wiki 布局(必填)
wikistring—docs / api / tutorial子站名,决定侧栏数据文件
langstringdefault_lang语言 code侧栏链接语言前缀(英文页必填)
closeAutoTocNumbool—true关闭 CSS 自动编号(手写编号时用)
encrypt_passwordstring—任意字符串侧栏顶层:整个 wiki 加密
encrypt_messagestring—任意字符串加密提示正文
encrypt_placeholderstring—任意字符串密码框 placeholder
encrypt_button_textstring—任意字符串解密按钮文案

6.4 边界与冲突

Wiki 页 tag 组件样式与布局宽度——两个真实踩过的坑:

  • 样式生效条件:tag 组件的 CSS 全部 scope 在 .post-container 后代选择器下。wiki <main> 必须同时带 post-container 和 wiki-main 类,组件才能获得样式。如果新增 tag 组件 CSS 时未以 .post-container 为前缀,该组件在 wiki 页将不可见。
  • 布局宽度:wiki <main> 需显式 width: 100%(_tags/_base/media.styl 中 .wiki-main { width: 100% }),否则 flex item 的 auto margin 会触发 shrink-to-fit,导致宽度随内容多少而不一致(内容少的页变窄)。max-width 由 .post-container 的全局规则控制,各 wiki 页宽度统一。

英文页漏 lang: en → 侧栏链接指向 /docs/ 而非 /en/docs/(详见「已知坑」坑 3)。

6.5 小结

效果汇总:/en/docs/setup/ 无英文版时显示中文内容 + 提示条 + noindex;有英文版则显示英文。

  • 两个必填键:layout: wiki + wiki: <子站>
  • 英文页三件套:lang: en 别漏
  • 整站加密:encrypt_password 一行搞定,encrypt_* 不会泄漏进侧栏
  • 测试:release-test.sh L2b(en 页面 200 + 回落提示条 + 评论 path 归一化 + 默认语言无前缀);L2y(wiki 加密页解密 + 侧栏无 structure/i18n/encrypt_ 泄漏);readmode-integration.test.js 断言 <main> 含 post-container 且 .wiki-layout 宽度跨页一致。

7. 相册功能

用途:/galleries/ 列表 + 单相册灯箱浏览,支持加密相册。

学习路径:最小相册 → 三种图片写法 → 全参数 → 边界

7.1 如何启用

效果:/galleries/ 出现相册卡片(封面 + 名称 + 描述),点进去是灯箱页(缩略图网格,点击全屏,caption 显示标题/描述)。

对应配置(source/_data/galleries.yml 顶层 + 相册页路由):

galleries:
  - name: 2020
    cover: /medias_webp/images/01.webp
    description: "我的图床"
    photos:
      - /medias_webp/images/02.webp

配套页面(hexo new gallery 2020 生成;缺它则列表卡片点进去 404):

---
title: 2020
layout: gallery
---

7.2 常用选项

① 单图(完整信息)

效果:该图在灯箱里显示自己的标题与描述,而不是回退相册描述。

对应配置:

photos:
  - url: /medias_webp/images/01.webp   # 图片路径(必填)
    title: "2020跨年"                   # 图片标题(可选,空则回退相册 description)
    desc: "拍摄于上海外滩"               # 图片详细描述(可选,空则不显示)

② 单图(简写)

效果:只写路径,灯箱标题回退为相册 description。

对应配置:

photos:
  - /medias_webp/images/02.webp

③ 整目录自动扫描

效果:目录下所有图片(webp/jpg/jpeg/png/gif/svg/avif)自动入册,标题为「图1、图2…」,文件名自然排序(01 < 02 < 10)。

对应配置:

photos:
  - dir: /medias_webp/featureimages/   # 目录路径(基准为 source/,必填)
    title_prefix: "图"                  # 标题前缀(可选)
    desc: "featureimages 系列"          # 目录统一描述(可选)
    sort: asc                          # asc=文件名升序(默认) / desc=降序

④ 加密相册

效果:进相册页先要密码,解密后灯箱才可浏览。

对应配置(相册页 front-matter):

---
title: private
layout: gallery
password: "{{GALLERY_PASSWORD}}"
---

7.3 全部可调项

键类型默认可选值/范围作用
galleries[].namestring—任意字符串(须与 index.md 的 title 一致)相册名
galleries[].coverstring—图片路径列表页封面
galleries[].descriptionstring—任意字符串相册描述(灯箱无标题时回退)
photos[].urlstring—图片路径单图(完整形式)
photos[].titlestring—任意字符串单图标题
photos[].descstring—任意字符串单图描述
photos[](字符串)string—图片路径单图简写形式
photos[].dirstring—目录路径(基准 source/)目录扫描形式
photos[].title_prefixstring—任意字符串目录形式标题前缀
photos[].sortstringascasc / desc目录形式排序
password(页面 front-matter)string—任意字符串加密相册

7.4 边界与冲突

  • 三种 photos 形式可混用,按数组顺序排列;多目录不跨目录混排(各目录内排序)。
  • name 与 source/galleries/<name>/index.md 的 title 必须对应,否则列表卡片点进去 404。
  • 解密后灯箱走统一 initGallery()(hbe-bundle onSuccess),见 §11.4。

7.5 小结

效果汇总:列表卡片 + 内页 lightGallery 灯箱浏览,加密相册解密后显示。

  • 最省事:dir: 一行扫整个目录
  • 要 caption:用 url + title + desc 三行
  • 要私密:页面 front-matter 加 password
  • 测试:/galleries/ 列表、/galleries/2020/ 灯箱 + caption、/galleries/private/ 加密解密 UI。

8. 搜索功能

用途:导航栏搜索框 + 弹窗实时结果,三引擎可切换。

学习路径:默认引擎 → 换引擎 / 缩索引 → 全参数 → 边界

8.1 如何启用

效果:导航栏出现搜索输入框(#searchInput),点击或 Ctrl+K(Mac ⌘+K)弹出搜索浮层,输入即出结果(auto 触发)。

对应配置(主题 _config.yml 的 search 块):

search:
  default: "algolia"     # 默认引擎:local | algolia | pagefind
  algolia:
    enable: true         # 总开关
  local:
    enable: true
    path: /search.json   # 本地索引输出路径(主题生成器 local-search.js)
  pagefind:
    enable: true         # 构建后 npx pagefind 扫描 public/ 生成静态索引

三引擎:local(本地 search.json)+ algolia(Algolia API)+ pagefind(Pagefind 索引)。

8.2 常用选项

① 全本地、不依赖外部服务

效果:搜索只 fetch /search.json,无外部请求;代价是索引随文章增长(大站首次打开稍慢)。

对应配置:

search:
  default: "local"
  algolia:
    enable: false
  pagefind:
    enable: false

若 default 指向的引擎 enable: false,会自动降级为第一个已启用的引擎并打警告日志。

② 缩小索引范围

效果:只索引文章标题(content: false),search.json 体积大幅下降,换来只能按标题命中。

对应配置:

search:
  local:
    enable: true
    field: [post]          # post | page | wiki | all | 数组组合
    content: false         # false = 仅匹配标题

③ 改成回车才搜

效果:输入过程中不发请求,按回车/点按钮才搜(长索引场景省流量)。

对应配置:

search:
  local:
    trigger: manual        # auto(输入即搜)| manual(回车/按钮)

④ Algolia 记录体积

效果:content_length: 2500 时中文每条约 7.5KB,1000 条 ≈ 7.5MB(Algolia 单批推荐 ≤10MB);设成 25000 会超 10KB/条上限,推送直接 400 失败。

对应配置:

search:
  algolia:
    content_length: 2500   # 正文纯文本截断(中文建议 2000-2500)
    excerpt_length: 300    # 摘要截断
    batch_size: 1000       # 每批推送条数

8.3 全部可调项

键类型默认可选值/范围作用
search.defaultstringalgolialocal / algolia / pagefind默认引擎(未启用则自动降级)
search.algolia.enablebooltruetrue / falseAlgolia 引擎开关
search.algolia.indexNamestringblog任意索引名Algolia 索引
search.algolia.content_lengthint25002000-2500(中文建议)正文截断字符数
search.algolia.excerpt_lengthint300150-300摘要截断字符数
search.algolia.batch_sizeint1000整批 ≤10MB每批推送条数
search.local.enablebooltruetrue / false本地引擎开关
search.local.pathstring/search.json输出路径索引文件位置
search.local.fieldarray[post, wiki]post / page / wiki / all / 数组组合搜索范围
search.local.contentbooltruetrue / false是否扫正文
search.local.formatstringstriptagsstriptags / raw索引正文格式
search.local.triggerstringautoauto / manual触发方式
search.local.top_n_per_articleint1-1 显示全部每篇命中片段数
search.pagefind.enablebooltruetrue / falsePagefind 引擎开关
search.pagefind.output_subdirstringpagefind相对 public/ 的子目录(勿用 _ 开头)索引输出目录

search.local.unescape / preload 两个键在配置里存在,但注释明确写着「当前实现未接入此选项,规划中」——改了不生效,其他值未验证。

8.4 边界与冲突

  • /search.json 是主题 generator 的构建产物,手改会被下一次构建覆盖。
  • 三引擎继承同一基类、共用 _highlight / _escapeHtml;新增引擎不继承它,结果高亮与转义风格就会和另外两个不一致。
  • #searchInput 由导航栏模板提供,与搜索配置无关——即使引擎全关,输入框仍在。
  • 加密文章通过 post-detail.ejs 的 data-pagefind-body 条件渲染自动排除在 Pagefind 索引外。

交互:

  • 弹窗:点击/聚焦导航栏搜索框打开;Ctrl+K(Mac ⌘+K)快捷键打开
  • 引擎切换:弹窗内下拉在 local / Algolia / Pagefind 三引擎间切换
  • 多语言过滤:按语言前缀过滤 + 回落去重(applyLangFilter)
  • 高亮统一:三引擎继承同一基类,共用 _highlight/_escapeHtml(结果高亮一致)

8.5 小结

效果汇总:#searchInput 可见;弹窗可打开;/search.json 可访问。

  • 最省心:local.enable: true + field: [post],零外部依赖
  • 要全站搜:field: [post, wiki],索引变大
  • 索引爆了:先 content: false,再考虑 trigger: manual
  • 测试:页面含 #searchInput;/search.json 可访问;弹窗可打开。

9. 评论系统

用途:文章/留言/友链/说说页底部评论框。

学习路径:选引擎 → 常用外观项 → 全参数 → 边界(Valine 已废弃)

9.1 如何启用

效果:页面底部出现 Waline 评论框(含 Markdown 编辑、Emoji、点赞),暗色模式自动跟随。

对应配置(主题 _config.yml 的 post.comments 段):

post:
  comments:
    enable: true
    type: waline        # utterances|disqus|gitalk|gitment|valine|waline|changyan|livere|remark42|twikoo|cusdis|giscus|discuss

单页覆盖(front-matter;comments 是开关、comment 是选工具,写错位置不生效):

---
comments: true        # 开关(false = 本篇关评论)
comment: waline       # 选工具(字符串)
---

9.2 常用选项

① 换头像与语言

效果:评论头像从 Gravatar 风格切换;界面文案跟随 lang。

对应配置:

waline:
  serverURL: 'https://waline.17lai.site'
  avatar: 'mp'        # ''/mp/identicon/monsterid/wavatar/retro/hide
  lang: zh-CN

② 暗色适配

效果:站点切暗色时评论框同步换肤,不出现白底刺眼区域。

对应配置:

waline:
  dark: 'html[data-user-color-scheme="dark"]'

③ 限制长度与分页

效果:评论超过 wordLimit 无法提交;列表每页 pageSize 条。

对应配置:

waline:
  wordLimit: 1000     # 0 = 不限制
  pageSize: 10

④ 单篇关评论

效果:该篇底部不渲染评论区(#comments 不出现)。

对应配置:

---
comments: false
---

9.3 全部可调项

键类型默认可选值/范围作用
post.comments.enablebooltruetrue / false文章评论总开关
post.comments.typestringwaline见上方枚举全局评论引擎
waline.enablebooltruetrue / falseWaline 客户端开关
waline.serverURLstring—Waline 服务端 URL评论后端地址
waline.avatarstringmp'' / mp / identicon / monsterid / wavatar / retro / hide头像风格
waline.langstringzh-CN语言 code界面语言
waline.darkstringhtml[data-user-color-scheme="dark"]CSS 选择器暗色适配钩子
waline.loginstringenableenable / force / disable登录模式
waline.wordLimitint10000 = 不限评论字数上限
waline.pageSizeint10任意正整数列表分页条数
waline.highlighterstringhanabi代码高亮器名评论内代码高亮
waline.highlight.enableboolfalsetrue / false关闭原始 highlighter
front-matter commentsbool—true / false单页开关
front-matter commentstring—waline 等单页引擎覆盖

9.4 边界与冲突

  • valine.enable: false 是停用态,valine.* 段仅作历史残留;实际引擎是 Waline。
  • front-matter 未写 comment 按全局 post.comments.type;comments 是开关、comment 是选工具,两个键不要混用(post-filter.js 按此约定处理默认值)。
  • 评论区依赖 #comments 容器,页面没注入容器时脚本不初始化——表现为空白而非报错。

已废弃:Valine(valine.enable: false)已停用,实际评论引擎为 Waline。

9.5 小结

效果汇总:Waline 评论框(Markdown/Emoji/点赞/后台管理)。

  • 最小可用:post.comments.enable + waline.serverURL 两项
  • 单篇关掉:front-matter comments: false
  • 换引擎:改 post.comments.type(其余引擎参数未在本文件展开,其他值未验证)
  • 测试:文章/msg/friends/bb 页含 #comments;waline 初始化脚本存在。

10. 暗色模式

用途:全站明暗切换,跟随系统或本地时间。

学习路径:开关 + 默认值 → auto 的判定顺序 → 全参数 → 边界

10.1 如何启用

效果:导航栏出现明暗切换按钮;auto 下系统是深色就用深色,系统无偏好时按本地时间 18:00–6:00 进暗色;切换按钮/悬浮面板手动选择后覆盖默认。

对应配置(主题 _config.yml):

dark_mode:
  enable: true
  default: auto   # auto | light | dark

10.2 常用选项

① 固定浅色(不跟随系统)

效果:无论系统/时间如何,首访都是浅色;读者仍可手动切暗。

对应配置:

dark_mode:
  default: light

② 固定深色

效果:首访即暗色(适合夜间向的内容站)。

对应配置:

dark_mode:
  default: dark

10.3 全部可调项

键类型默认可选值/范围作用
dark_mode.enablebooltruetrue / false暗色模式总开关
dark_mode.defaultstringautoauto / light / dark初始模式

10.4 边界与冲突

  • auto 的判定顺序固定:先系统 prefers-color-scheme,系统无明确偏好时才用本地时间 18:00–6:00。
  • 手动切换(按钮或悬浮面板)写入本地偏好并覆盖 default;清掉本地存储后回落 default。
  • 属性驱动:[data-user-color-scheme="dark"] 换一整套 CSS 变量,硬编码颜色的组件不会跟随(见 docs/dark-mode-principles.md)。
  • 关掉 enable 时按钮消失,但历史本地偏好仍存在——读者本地存的暗色记录需要清 localStorage 才回浅色。

10.5 小结

效果汇总:[data-user-color-scheme="dark"] 属性驱动全套 CSS 变量切换,图片/图表自适应。

  • 默认就好:enable: true + default: auto
  • 改默认:改 default 一个值
  • 新组件不跟随:用 var(--*),别硬编码颜色(见 §15)
  • 测试:CSS 含 [data-user-color-scheme="dark"] 规则;切换按钮可切换明暗。

11. 加密文章

用途:按标签批量加密文章,读者输密码解密;也可整篇/整 wiki 加密。

学习路径:标签加密 → 文案与密钥缓存 → 全参数 → 边界 + 解密后刷新

11.1 如何启用

效果:打了加密标签的文章,页面上只有解密 UI(#hbePass + 密码框),密码正确后正文才渲染。

对应配置(根 _config.yml 的 encrypt 段):

encrypt:
  enable: true
  abstract: 有东西被加密了, 请输入密码查看.
  message: 您好, 这里需要密码.
  tags:
    - {name: 私人, password: "{{TAG_PASSWORD}}" }
  wrong_pass_message: 抱歉, 这个密码看着不太对, 请再试试.

常用选项:

① 按标签加密

效果:所有 tags: [私人] 的文章自动加密,无需逐篇配置;去掉标签后需重新构建才恢复明文。

对应配置:

encrypt:
  tags:
    - {name: 私人, password: "{{PWD_1}}"}
    - {name: mywork, password: "{{PWD_2}}"}

② 单篇密码

效果:该篇用自己写的密码,不受标签规则影响;password: "" 可让某篇在标签下也不加密。

对应配置(front-matter):

---
tags:
  - 私人
password: "{{POST_PASSWORD}}"
default_abstract: 本文已加密      # 加密提示文案(可选)

③ 解密后免重输

效果:刷新页面跳过密码框(AES 密钥缓存在 localStorage 的 hbe.v4.<post-permalink-hash>)。

对应配置:

encrypt:
  autoSave: true       # 全局开启;也可在单篇 front-matter 写 autoSave: true

④ 稳定 salt(静态部署平台)

效果:每次干净构建 salt 不变,读者本地缓存不会因重新部署而失效;AES-GCM nonce 仍每次随机(避免 nonce reuse)。

对应配置:

encrypt:
  stableSalt: true
  kdf:
    iterations: 600000   # 默认 250000;下限 100000,推荐 ≥600000(OWASP 2023)

11.2 全部可调项

键类型默认可选值/范围作用
encrypt.enablebooltruetrue / false加密功能总开关
encrypt.abstractstring有东西被加密了, 请输入密码查看.任意字符串摘要区提示
encrypt.messagestring您好, 这里需要密码.任意字符串解密框提示
encrypt.tagsarray—{name, password} 对象数组命中这些标签的文章自动加密
encrypt.wrong_pass_messagestring抱歉, 这个密码看着不太对, 请再试试.任意字符串密码错误提示
encrypt.autoSavebooltrue(本站配置值)true / false派生密钥缓存到 localStorage
encrypt.stableSaltboolfalsetrue / false跨 clean build 稳定 PBKDF2 salt
encrypt.kdf.iterationsint250000≥100000,推荐 ≥600000PBKDF2 迭代次数
front-matter passwordstring—任意字符串("" = 不加密)单篇密码
front-matter autoSavebool—true / false单篇覆盖密钥缓存

11.3 边界与冲突

  • 按标签匹配生效,不是逐篇开关:加标签即加密,去掉标签需重新构建才恢复明文。
  • 加密态不显示 TOC 是设计行为(见「已知坑」坑 2),测试别写死「TOC 必须存在」。
  • 解密后必须走 hbe-bundle onSuccess 统一 refresh,否则正文里的代码块/灯箱/Mermaid 不会初始化。
  • kdf.iterations 低于推荐值构建会打印 warning(不是错误,构建仍成功)。

11.4 解密后统一刷新(2026-09-09 新增)

效果:解密瞬间,正文里的代码块工具栏、图片灯箱、Mermaid 图、瀑布流布局一次性全部就位(解密前 DOM 里根本没有这些元素)。

对应机制(无配置项,hbe-bundle onSuccess 统一触发):

组件解密后动作
代码块(code-area)codeWidget() 扫描新 pre 生成 toolbar(语言标签/复制/收起/全屏)
图片灯箱articleInit() 包装 img → .img-item + lightGallery 绑定(post/wiki 共用)
Mermaid 图表events.refresh() 触发 refresh 回调重渲染(data-processed 幂等)
布局(AOS/Masonry)boot.refresh() → refreshLayout 重排

统一触发点:hbe-bundle onSuccess(所有加密场景:文章/相册/wiki/片段共用)。各监听器只保留特有处理:

  • post:tocbot reinit(文章目录)
  • wiki:TOC 加密态移除 + tocbot reinit
  • 相册:initGallery()

幂等保护:codeWidget data-code-processed、mermaid data-processed、articleInit destroy 旧实例重建——重复调用不产生重复元素。

11.5 小结

效果汇总:文章页显示解密 UI(#hbePass),输入正确密码后渲染正文。

  • 最小可用:enable: true + tags: [{name, password}]
  • 别用明文密码:配置里的密码应走占位符/私密配置,不进公开仓库
  • 读者嫌烦:autoSave: true
  • Vercel/CF Pages:stableSalt: true,否则每次部署缓存全失效
  • 测试:tag 加密文章含 #hbePass;解密后 .code-toolbar 数量 = .code-area 数量;图片包装 .img-item + 容器 lg-uid 属性存在。

12. RSS / 站点地图

用途:输出订阅源(RSS/Atom/JSON Feed)与搜索引擎抓取用的 sitemap。

学习路径:最小配置 → 关掉某格式 → 全参数 → 边界

12.1 如何启用

效果:站点根目录出现 /rss.xml、/atom.xml、/feed.json(订阅源)与 /sitemap.xml、/baidusitemap.xml(抓取用)。

对应配置(根 _config.yml):

feed:
  limit: 10
  order_by: "-date"
  tag_dir: false
  category_dir: "categories"
  rss:
    enable: true
    output: "rss.xml"
  atom:
    enable: true
    output: "atom.xml"
  jsonFeed:
    enable: true
    output: "feed.json"

sitemap:
  path: sitemap.xml

baidusitemap:
  path: baidusitemap.xml

12.2 常用选项

① 只要 Atom

效果:/rss.xml 与 /feed.json 不再生成,只剩 /atom.xml(现代阅读器都吃 Atom)。

对应配置:

feed:
  rss:
    enable: false
  atom:
    enable: true
  jsonFeed:
    enable: false

② 只导最近 20 篇

效果:feed 里最多 20 条,旧文章不进订阅源(新读者不会被历史洪水淹没)。

对应配置:

feed:
  limit: 20
  order_by: "-date"

③ 换输出路径

效果:订阅源改到自定义文件名;改完必须 clean 构建,否则旧的 atom.xml / sitemap.xml 还留在 public/。

对应配置:

sitemap:
  path: sitemap-custom.xml

12.3 全部可调项

键类型默认可选值/范围作用
feed.limitint10任意正整数每条 feed 收录文章数
feed.order_bystring-date任意字段(前缀 - 倒序)排序
feed.tag_dirboolfalsetrue / false是否输出标签 feed
feed.category_dirstringcategories目录名分类 feed 目录
feed.rss.enablebooltruetrue / falseRSS 输出开关
feed.rss.outputstringrss.xml输出文件名RSS 路径
feed.atom.enablebooltruetrue / falseAtom 输出开关
feed.atom.outputstringatom.xml输出文件名Atom 路径
feed.jsonFeed.enablebooltruetrue / falseJSON Feed 输出开关
feed.jsonFeed.outputstringfeed.json输出文件名JSON Feed 路径
sitemap.pathstringsitemap.xml输出文件名通用站点地图
baidusitemap.pathstringbaidusitemap.xml输出文件名百度站点地图

12.4 边界与冲突

  • feed.*.output 只管各自那一份;三份互相独立,关一个不影响另两个。
  • 百度站点地图是另一个插件的产物(baidusitemap),与 sitemap.path 没有关联。
  • 改 path / output 后要 clean 构建,否则旧文件仍留在 public/。
  • 主题侧 RSS 模板是 themes/matery/layout/_partial/rss.ejs(自定义模板),改样式在模板不在配置。

12.5 小结

效果汇总:/rss.xml、/atom.xml、/feed.json、/sitemap.xml 均可访问。

  • 默认就好:三种订阅格式都开着,兼容性最好
  • 减产物:rss/jsonFeed 想关就关
  • 改完不生效:clean(坑 5)
  • 测试:/rss.xml、/atom.xml、/feed.json、/sitemap.xml 均可访问。

13. PWA

用途:可安装 + 离线缓存 + 版本更新自动刷新。

学习路径:总开关 → 缓存清单 → 全参数 → 边界(localhost 豁免)

13.1 如何启用

效果:Chrome 地址栏出现安装按钮,安装后独立窗口运行;断网时已缓存页面仍可打开;站点发新版后首次访问自动刷新一次。

对应配置(主题 _config.yml):

pwa:
  enable: true

13.2 常用选项

① 改缓存清单

效果:列表里的资源在首次访问时预缓存(/css/main.css、js/boot.js 等),断网可用;不在列表里的按需缓存。

对应配置(userConfig/sw.tmp.js):

var cacheList = [
  '/css/main.css{urlVersion}',
  '/css/highlight.css{urlVersion}',
  '{cdnUrl}/js/boot.js{urlVersion}',
  '{cdnUrl}/js/events.js{urlVersion}'
];

② 缓存版本号

效果:{uniqueIdentifier} 被 CI 替换为构建时间戳(17lai-cache-{时间戳}),键一变即视为新版本,触发一次自动刷新并清掉旧 17lai-cache-*。

对应配置:

const cacheStorageKeyPrefix = '17lai-cache-';
const cacheStorageKey = `17lai-cache-{uniqueIdentifier}`;

13.3 全部可调项

键类型默认可选值/范围作用
pwa.enablebooltruetrue / falsePWA 总开关
userConfig/sw.tmp.js模板—CI 注入 {uniqueIdentifier} 后生成 source/sw.jsService Worker 模板
cacheListarray见模板资源路径数组预缓存清单
cacheStorageKeystring17lai-cache-{uniqueIdentifier}前缀 + 标识缓存键(变更即新版本)

pwa 段内的 manifest(名称/图标/主题色)由构建注入,具体键名未在本文件展开,其他值未验证。

13.4 边界与冲突

  • source/sw.js 是构建产物,手改会被 CI 覆盖;要改逻辑得改 sw.tmp.js。
  • localhost 豁免 SW 注册(测试用),验证离线能力需用域名(如测试域)。
  • 缓存键一变即视为新版本,触发一次自动刷新;skipWaiting + clients.claim() 让新 SW 立即接管,并清掉旧 17lai-cache-*。
  • 改了 cacheList 但版本号没变(非 CI 构建),浏览器不会感知更新。

13.5 小结

效果汇总:可安装(manifest)、离线缓存静态资源、版本更新自动刷新。

SW 版本对比自动刷新:注册 SW 时监听 updatefound(新 SW installed)与 controllerchange,对比新旧 cacheStorageKey:变化则自动刷新一次(不弹窗,用户无需手动刷新)。

  • 默认就好:pwa.enable: true,其余交给 CI
  • 加预缓存:往 cacheList 追加路径,记得带 {urlVersion}
  • 本地测不到:localhost 豁免,用测试域
  • 测试:/sw.js 可访问,SW 注册正常(localhost 豁免)。

14. SEO 结构化数据(JSON-LD + Open Graph)

用途:为搜索引擎与社交平台提供结构化元数据:JSON-LD 富结果 + Open Graph / Twitter Card 分享预览。

学习路径:它自动输出什么 → 实体与 @id → 全参数 → 边界(无开关)

14.1 如何启用

效果:每个页面 <head> 里有一份 <script type="application/ld+json">(@graph 单 script:WebSite + Person + Organization,文章页再追加 BlogPosting),另有 og:type/title/url/site_name/description/image 与 twitter:card=summary_large_image。分享到社交平台显示标题/描述/封面图卡片。

对应配置:无独立开关——随 head.ejs 无条件输出。

layout/_partial/common/structured-data.ejs   # JSON-LD(@graph)
layout/_partial/common/head.ejs              # OG / Twitter Card

14.2 常用选项

① 站点搜索框富结果

效果:Google 结果里可能出现站内搜索框(Sitelinks Searchbox),提交后直接跳站内搜索。

对应配置:由 WebSite 实体里的 SearchAction 携带,指向站内搜索 URL(模板内已固定,无独立键)。

② 作者/发布者关联

效果:BlogPosting.author 指 #person、publisher 指 #organization,富结果校验不会报"实体缺失"。

对应配置:@id 互相引用(改 @id 必须同步改引用方)。

14.3 全部可调项

键类型默认可选值/范围作用
开关——无(随 head.ejs 无条件输出)无独立配置项
@graph 实体——WebSite(含 SearchAction)、Person、Organization;文章页追加 BlogPosting结构化数据
描述截断—210 字符—strip_html 后截断长度

14.4 边界与冲突

  • 没有开关:要关只能改 structured-data.ejs / head.ejs 模板。
  • @id 互相引用(publisher→#organization、author→#person),改 @id 需同步改引用方,否则富结果校验失败。
  • JSON-LD 与 OG/Twitter 是两套独立输出,缺一套不影响另一套。
  • 描述经 strip_html 后按 210 字符截断——想改截断长度得改模板,不是配置。

14.5 小结

效果汇总:页面含 application/ld+json;文章页含 BlogPosting;含 og:type / twitter:card 标记。

  • 零配置:默认就有,不用开
  • 要改内容:改 structured-data.ejs,注意 @id 双向引用
  • 分享没图:查文章 img / 封面图,不是 JSON-LD 的问题
  • 测试:页面含 application/ld+json;文章页含 BlogPosting;含 og:type / twitter:card 标记。

15. 设计令牌与 CSS 变量架构

用途:统一设计令牌(圆角/阴影/过渡/间距/字号),经三层传递链落到 CSS 自定义属性供全站引用,避免硬编码。

学习路径:令牌在哪定义 → 改一处全站生效 → 全参数 → 边界(三层链)

15.1 如何启用

效果:卡片圆角统一 8px、按钮 3px、弹窗 12px;卡片默认 shadow_md、hover shadow_lg;间距按 4px 递增;正文字号随偏好面板的 --font-scale 等比缩放。

对应配置(主题 _config.yml 的 design 段):

design:
  radius_sm: 3px          # 按钮/小标签
  radius_md: 8px          # 卡片/便签
  radius_lg: 12px         # 弹窗
  shadow_md: "0 2px 5px 0 rgba(0,0,0,0.16)"
  shadow_lg: "0 5px 11px 0 rgba(0,0,0,0.18)"
  transition_fast: 0.2s
  space_1: 4px            # 4px 基准递增
  font_size_md: 1.125rem  # rem 制,随 --font-scale 缩放

三层传递链:

_config.yml design.* → theme-config() → Stylus 变量 → :root CSS 自定义属性 → 组件 var(--*)
  • 方案层:source/css/_schemes/matery.styl(对齐 NexT _schemes/ 机制,未来可新增方案)
  • 字号:根字号 calc(var(--font-size-base) * var(--font-scale)),偏好面板调 --font-scale 全站 rem 等比缩放

15.2 常用选项

① 改圆角风格

效果:把三档圆角一起调小 → 全站卡片/按钮/弹窗同步变"方正";只改 radius_md 会导致按钮与卡片风格不一致。

对应配置:

design:
  radius_sm: 2px
  radius_md: 4px
  radius_lg: 6px

② 调暗色阴影

效果:暗色模式下阴影不显脏(浅色阴影在深底上会发灰),卡片浮起感保留。

对应配置:

design:
  shadow_md: "0 2px 5px 0 rgba(0,0,0,0.16)"
  shadow_md_dark: "0 2px 5px 0 rgba(0,0,0,0.45)"

15.3 全部可调项

键类型默认可选值/范围作用
design.radius_smlength3px任意 CSS 长度按钮/小标签圆角
design.radius_mdlength8px任意 CSS 长度卡片/便签圆角
design.radius_lglength12px任意 CSS 长度弹窗圆角
design.radius_pilllength999px任意 CSS 长度胶囊圆角
design.radius_fulllength50%任意 CSS 长度圆形
design.shadow_smstring"0 1px 3px 0 rgba(0,0,0,0.12)"合法 box-shadow悬浮微阴影
design.shadow_mdstring"0 2px 5px 0 rgba(0,0,0,0.16)"合法 box-shadow卡片默认
design.shadow_lgstring"0 5px 11px 0 rgba(0,0,0,0.18)"合法 box-shadow卡片 hover
design.shadow_xlstring"0 10px 30px 0 rgba(0,0,0,0.2)"合法 box-shadow弹窗/浮层
design.shadow_*_darkstring0 1px 3px …0.4 等合法 box-shadow暗色模式对应阴影
design.transition_fasttime0.2s任意 CSS 时间悬停反馈
design.transition_normaltime0.3s任意 CSS 时间面板/弹窗
design.gradient_primarystringlinear-gradient(to right, #4facfe, #00f2fe)合法 linear-gradient导航/按钮渐变
design.gradient_accentstringlinear-gradient(135deg, #667eea, #764ba2)合法 linear-gradient卡片装饰渐变
design.space_1length4px任意 CSS 长度间距基准
design.space_2length8px任意 CSS 长度间距刻度
design.space_3length12px任意 CSS 长度间距刻度
design.space_4length16px任意 CSS 长度间距刻度
design.space_5length24px任意 CSS 长度间距刻度
design.font_size_xslength0.75remrem 值字号刻度
design.font_size_smlength0.875remrem 值字号刻度
design.font_size_mdlength1.125remrem 值正文字号(随 --font-scale)
design.font_size_lglength1.25remrem 值字号刻度
design.font_size_xllength1.5remrem 值字号刻度
design.font_size_2xllength2remrem 值字号刻度

15.4 边界与冲突

  • 令牌经三层链落到 :root 自定义属性,组件必须用 var(--*) 才跟随;硬编码值不会变。
  • font_size_md 是 rem:改它等于改全站字号基准,偏好面板的 --font-scale 会在其上再乘一次。
  • 映射在方案层 source/css/_schemes/matery.styl;改令牌名要同步改 scheme,否则变量悬空、组件拿不到值。
  • 暗色阴影必须用 *_dark 那一套;直接复用亮色阴影在深底上几乎看不见。

15.5 小结

效果汇总:组件统一引用令牌(圆角/阴影/间距一致),改 design 段一处即全站生效。

  • 改圆角/阴影/间距:改 design 段,别去组件里改
  • 暗色要单独给值:*_dark 不能省
  • 新组件:var(--*),硬编码就不跟随主题
  • 测试:CSS 含 --font-scale 规则与根字号 calc(... * var(--font-scale))。

⚠️ 已知坑

坑 1:标题编号双重叠加

现象:标题渲染为「1. 1. 概述」(两个编号)。

原因:主题 CSS 自动给 h2–h6 加编号(默认开启),手写编号(如 ## 1. 概述)会叠加。

正确做法:手写编号的页面在 front-matter 加 closeAutoTocNum: true;不手写编号的页面不要加该字段。

本文已正确设置 closeAutoTocNum: true(front-matter 第 19 行)。

坑 2:加密 ↔ TOC 冲突

现象:wiki 整体加密时 TOC 按设计不显示。

原因:加密模式下 TOC 组件被跳过(设计如此)。

正确做法:相关测试断言勿写死「TOC 必须存在」。加密 wiki 页面不显示 TOC 是正常行为,不是 bug。

坑 3:Wiki 英文页漏 lang: en

现象:侧栏链接指向 /docs/ 而非 /en/docs/。

原因:wiki_sidebar 使用 page.lang || default_lang,省略 lang: en 则回落到默认语言路径。

正确做法:所有 source/en/tutorial/ 下的页面必须写 lang: en。验证:for f in source/en/tutorial/**/*.md; do grep -L 'lang: en' "$f"; done。

坑 4:_config.tmp.yml 注释行保留

现象:尝试删除注释行瘦身,被用户否决并 100% 回滚。

原因:用户明确声明注释行是有用储备(未来可能恢复)。

正确做法:永远不要删除或修改 userConfig/_config.tmp.yml 中的注释行。

坑 5:改配置后必须 clean 构建

现象:改了配置但页面不变。

原因:Hexo 缓存机制不会自动检测配置变更。

正确做法:改配置或 source/_data/* 后必须 npm run clean && npm run build。构建日志中出现 INFO Deleted database. 和 INFO Deleted public folder. 表示 clean 成功执行。

坑 6:source/_data/* 增量缓存误判

现象:修改 source/_data/friends.yml 等数据文件后,页面空白或数据不更新。

原因:Hexo 增量缓存误判「已缓存未变化」(Cache 哈希更新但 Data model 未写入)。

正确做法:

  • 本地:rm -f db.json && npm run build
  • 容器:tools/cicd.sh -d(内置 clean + generate + 配置注入)
  • 容器内 pm2 restart 不够:pm2 内存缓存 db.json 旧 content

附:布局页面功能速查表

功能页面/位置关键配置自动化断言标记
上下篇文章底部无键(post.prev_next.enable 未被读取)#prenext-posts
版权声明文章底部post.copyright.enable / license#reprint-statement
时效提示文章开头post.outdateoutdate
TOC右侧/侧栏toc(顶层)#toc-content
广告位TOC 下方post.advertisements.tgCard
分类雷达图/categories/无键(categories.ejs)canvas(Matery.echarts 明暗切换)
标签云/词云/tags/wordcloud.minHeight / heightPerWord.tag-chips / #tag-wordcloud
ArtiTalk 说说/about/artitalk.enable: false(默认禁用)LeanCloud 后端
我的项目/about/myProjects.enable: true.my-projects
我的技能/about/mySkills.enable: true.my-skills
关于页相册/about/myGallery.enable: true.my-gallery
归档日历/archives/postCalendar: true#post-calendar
在线音乐/musics/musics + failover.aplayer(tools/tests/musics-integration.test.js L2i)
归档时间线/archives/无键(archive.ejs)#cd-timeline
关于页图表/about/无键(about.ejs)canvas
相册/galleries/galleries.yml灯箱 + 加密 UI
搜索导航栏search#searchInput / search.json
评论文章/msg 等post.comments#comments
暗色模式全局dark_modedata-user-color-scheme
加密标签驱动encrypt.tags#hbePass
RSS/Sitemap根路径feed / sitemap/rss.xml 等
PWA全局pwa + sw.tmp.js/sw.js
SEO<head>structured-data.ejs + head.ejsapplication/ld+json / og:type
设计令牌全局design 段 + _schemes/--font-scale

参考&致谢

系列教程

全部文章RSS订阅

Hexo 主题功能测试系列


Hexo主题功能测试-布局与页面篇
发布于
2026年8月7日
许可协议。转载请注明来源
赏
评论
数据加载中 ...
 上一篇

阅读全文

Hexo主题功能测试-Markdown语法篇
Hexo主题功能测试-Markdown语法篇 Hexo主题功能测试-Markdown语法篇
主题 Markdown 语法完整教程与测试:16 个 markdown-it 插件全覆盖(emoji/缩写/脚注/插入/上下标/高亮/任务列表/表格增强/图片尺寸/容器/定义列表/数学公式/中文排版)。每个插件按「最小示例 → 常用变体 →
2026-08-08
下一篇 

阅读全文

Hexo主题功能测试-交互视觉篇
Hexo主题功能测试-交互视觉篇 Hexo主题功能测试-交互视觉篇
主题交互与视觉功能完整教程与测试:代码块增强、图片灯箱、打字机、页面特效、繁简转换、阅读进度、返回顶部、打赏弹窗、打印样式。每项含用途、完整参数、多用法示例、实现效果,供自动化测试断言。
2026-08-07