Hexo NexT 静态资源本地自托管、KaTeX 公式渲染与移动端适配 | 排坑笔记

1. 背景与问题表现

在博客日常维护中,减少外部资源依赖并保障访问稳定性,能明显提升浏览体验。

在实际排查中,主要遇到了以下三类影响阅读体验的问题:

故障类别典型表现影响
外部资源加载cdnjs 静态请求偶发超时字体图标显示为空白方块、图片懒加载失效
数学公式排版KaTeX 下标字符沉底断行、与正文文字重合公式排版错位,影响技术推导理解
移动端交互超长代码截断无法滑动、搜索弹窗关闭按钮被遮挡移动端无法完整查看代码,且搜索弹窗不易关闭

2. 静态资源本地自托管(移除外部公共 CDN 依赖)

2.1 NexT 主题 vendors 机制解析

NexT 主题内置了一套静态资源厂商分发逻辑,位于 node_modules/hexo-theme-next/scripts/events/lib/vendors.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// NexT 内部 vendors 处理片段
let { plugins = 'cdnjs' } = vendors;
for (const [key, value] of Object.entries(dependencies)) {
// 若博主在 _config.next.yml 中显式指定了字符串路径,则优先使用
if (vendors[key] && typeof vendors[key] === 'string') {
vendors[key] = {
url: url_for.call(hexo, vendors[key])
};
continue;
}
// 否则自动拼接 cdnjs 境外公共 CDN 路径
const links = getVendors({ ...value, ... });
vendors[key] = { url: links[plugins] };
}

当博主未在配置文件中配置本地覆盖时,主题便会将 animejslozad.jsfancyboxfontawesomekatex 等全部指向 cdnjs.cloudflare.com

2.2 资产提取与本地归档目录设计

我们将全部静态依赖进行本地收敛,并在 source/ 下构建清晰的库目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
source/
├── fonts/ # 根目录 KaTeX 字体备用镜像
├── webfonts/ # 根目录 FontAwesome 字体备用镜像
├── images/
│ └── cc-by-nc-sa.svg # 官方 CC 协议矢量徽章
└── lib/
├── anime/
│ └── anime.min.js # 3.2.1 动效引擎
├── font-awesome/
│ ├── css/all.min.css
│ └── webfonts/ # FontAwesome 完整家族 (.woff2)
├── katex/
│ ├── katex.min.css
│ ├── katex.min.js
│ └── fonts/ # KaTeX 数学字体家族
└── lozad/
└── lozad.min.js # 1.16.0 懒加载核心

2.3 生产构建打包器(hexo-filter-optimize)的路径陷阱与双重目录架构

当本地托管部署到生产环境时,开启了 hexo-filter-optimize 插件进行 CSS 合并与压缩。这里存在一个极其隐蔽的路径解析陷阱:

  1. 未打包时,FontAwesome CSS 位于 /lib/font-awesome/css/all.min.css,其内部规则为 url(../webfonts/fa-solid-900.woff2),浏览器正常寻址到 /lib/font-awesome/webfonts/...
  2. 开启 filter_optimize.css.bundle: true 后,插件会将全站 CSS 合并至根目录的 /style.css。此时 ../webfonts/ 会以根目录为基准,被浏览器解析为 /webfonts/fa-solid-900.woff2,导致字体图标 404。

解决方案:建立双重目录结构
我们在 source/lib/font-awesome/webfonts/ 放置字体的同时,同步在 source/webfonts/source/fonts/ 部署副本。这样无论是在开发调试环境(直接加载单文件)还是在生产构建环境(打包至 /style.css),字体请求始终都能精准命中,返回 HTTP 200。

2.4 主题配置中心映射配置

_config.next.yml 中配置如下本地供应商规则:

1
2
3
4
5
6
7
8
9
10
vendors:
local_search: /js/search.js
fancybox_js: /lib/fancybox/fancybox.umd.js
fancybox_css: /lib/fancybox/fancybox.css
fontawesome: /lib/font-awesome/css/all.min.css
katex: /lib/katex/katex.min.css
copy_tex_js: /lib/katex/contrib/copy-tex.min.js
anime: /lib/anime/anime.min.js
lazyload: /lib/lozad/lozad.min.js
creative_commons: /images/cc-by-nc-sa.svg

同时清理 source/_data/head.swig 中无用的预解析标记:

1
- <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>

3. 数学公式排版修复:KaTeX 跨版本渲染对齐

3.1 下标塌陷与垂直对齐错位根因

Hexo 渲染 Markdown 公式主要依赖 hexo-renderer-markdown-it-plus 下挂的 markdown-it-katex(版本通常停留在 KaTeX 0.6.0~0.12.0 时代)。其编译生成的 HTML 节点特征如下:

1
2
3
4
5
6
7
8
<span class="katex">
<span class="katex-mathml">...</span>
<span class="katex-html" aria-hidden="true">
<span class="base textstyle uncramped">
<span class="mord"><span class="mord mathit">Q</span><span class="vlist"><span>...</span></span></span>
</span>
</span>
</span>

而现代化 KaTeX(0.16+ / 0.18+)为了更符合 W3C 规范,将多行垂直基准类升级为 .vlist-t.vlist-r,不再给老旧的 .vlist > span 赋予默认高度限制。结果导致旧版编译出的下标节点高度变为 auto,直接沉降并撑高父容器,引发大面积折行。

3.2 样式层向下兼容注入方案

source/_data/styles.styl 中追加向下兼容对齐补丁:

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
/* 1. 强制隐藏 KaTeX 纯文本 MathML 节点,避免与视觉公式重叠或被阅读器双重朗读 */
.katex .katex-mathml {
display: none !important;
position: absolute;
clip: rect(1px, 1px, 1px, 1px);
padding: 0;
border: 0;
height: 1px;
width: 1px;
overflow: hidden;
}

/* 2. 补齐旧版 markdown-it-katex 编译输出的下标垂直定位基线 */
.katex .katex-html .vlist {
display: inline-block !important;
position: relative !important;
}

.katex .katex-html .vlist > span {
display: block !important;
height: 0 !important;
position: relative !important;
vertical-align: 0 !important;
}

.katex .katex-html .vlist .baseline-fix {
display: inline-table !important;
table-layout: fixed !important;
}

/* 3. 移动端块级公式横向防截断与平滑滚动容器 */
.katex-display {
overflow-x: auto !important;
overflow-y: hidden !important;
-webkit-overflow-scrolling: touch !important;
padding: 0.8em 0 !important;
max-width: 100% !important;
}

3.3 Markdown 转义字符吞噬排坑

在部分博文的公式撰写中,若乘法符号前带有制表符(Tab)缩进,Markdown 解析器会将 \t 转义为跳格字符,导致公式内的 \times 变成 imes。必须确保行内公式严格使用单个反斜杠与空格分隔:

1
2
- 长度为 $\mathcal{O}(L \	imes D)$
+ 长度为 $\mathcal{O}(L \times D)$

4. 移动端阅读与交互细节优化

4.1 代码块触摸滑动与 Sticky Gutter

在小屏设备上,长代码块不能强制折行(会破坏代码结构与逻辑),必须允许水平滑动;但水平滑动时若行号跟着滚走,阅读体验会大打折扣。

source/_data/styles.styl 中重构代码容器:

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
/* 开启表格级别横向滚动容器 */
figure.highlight .table-container {
overflow-x: auto !important;
-webkit-overflow-scrolling: touch !important;
max-width: 100% !important;
position: relative !important;
}

/* 行号列绝对锚定在左侧 (Sticky Gutter) */
figure.highlight .gutter {
position: sticky !important;
left: 0 !important;
z-index: 2 !important;
background: #f8fafc !important; /* 浅色实体背景,防止滚动时代码穿透 */
}

html[data-theme='dark'] figure.highlight .gutter,
html.dark-mode figure.highlight .gutter {
background: #18181c !important; /* 深色实体背景 */
}

/* 右侧保留安全距离,避免最右侧代码被右上角复制按钮与语言徽章遮盖 */
figure.highlight .code pre {
padding: 10px 48px 10px 14px !important;
white-space: pre !important;
}

4.2 移动端全屏搜索弹窗与浮动控件的状态解耦

在移动端视口(如 375px)中,固定在屏幕右上角的主题切换微按钮(Theme Toggle FAB)与全屏搜索弹窗的关闭按钮 .popup-btn-close 发生像素级重叠。用户在尝试关闭搜索时,总会误触主题切换按钮。

通过 CSS 状态机建立状态联动,在搜索弹窗激活时将控件平滑隐去:

1
2
3
4
5
6
7
8
/* 当全站搜索激活时,优雅隐去主题切换按钮,彻底消除在移动端与右上角关闭按钮发生重叠遮挡 */
body.search-active .theme-toggle-btn {
opacity: 0 !important;
visibility: hidden !important;
pointer-events: none !important;
transform: scale(0.85);
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s;
}

5. 效果验证与数据指标

完成改造后,我们执行了 npx hexo clean && npx hexo generate 并通过 Headless 浏览器进行性能追踪与渲染断言:

1
2
3
4
5
INFO  Cleaned database and cache
INFO Generating HTML, CSS, JS and static assets...
INFO Update Optimize CSS: main.styl [ 19.87% saved ]
INFO Update Optimize JS: bundle.js [ 42.15% saved ]
INFO 768 files generated in 652 ms

5.1 网络请求层验证

在浏览器 DevTools Network 面板与自动化测试抓包中验证:

  1. 外部 CDN 请求数:对 cdnjs.cloudflare.com 的请求降至严格的 0 个
  2. 核心资源耗时
    • 字体文件:本地 /webfonts/fa-solid-900.woff2(HTTP 200,耗时 4ms)。
    • 脚本资源:anime.jslozad.js 自动并入本地 /bundle.js 一并送达。
    • 协议徽章:/images/cc-by-nc-sa.svg(HTTP 200,耗时 3ms)。
  3. 国内首屏 FCP(首次内容绘制):平均白屏耗时由原先的 1.8s 下降至 380ms 左右,彻底消除了由于 Cloudflare Anycast 路由丢包导致页面假死的隐患。

5.2 渲染层视觉对齐验证

  1. KaTeX 公式:下标字符自然依附于变量右下方,无多余折行,MathML 原生文本被规整裁剪,移动端长公式支持顺滑横向触摸滚动。
  2. 代码块交互:移动端长单行代码可左右自由拖拽,行号始终固定在左侧,复制按钮与代码主体留有充足呼吸间距。
  3. 搜索弹窗:移动端点击搜索,主题按钮淡出,关闭按钮处于可触控黄金区域,点击一键平滑退回主屏。

6. 总结与体会

  1. 静态资源本地托管
    博客依赖的通用字体与常用脚本,优先放置在本地或主域下托管,能有效减少因外部网络波动导致的页面渲染异常。
  2. 构建打包器的相对路径
    在启用如 hexo-filter-optimize 这类会将样式、脚本合并至根目录的打包工具时,需留意 CSS 内部相对引用的层级变化,通过建立静态目录镜像保证不同构建模式下的资源可达。
  3. 跨版本渲染的兼容处理
    当底层 Markdown 解析插件与前端样式规范存在版本差异时,可以通过自定义样式表进行定向适配与补齐,无需直接改动 node_modules 源码,便于后续维护。