我所理解的 Hexo 最佳实践(8):让 Hexo 支持 Markdown 扩展语法
- 用文件名管理文章,用稳定字段生成网址
- 让本地预览和公开链接共用一份资源
- 用系列目录组织连载正文
- 在首页分页前聚合连载
- 让列表排序与全站翻篇沿用同一顺序
- 把源码工作区嵌进 Hexo 文章
- 用 Vue 重写主题并保留页面交互
- 让 Hexo 支持 Markdown 扩展语法(当前)
把 > [!NOTE] 写进 Hexo 文章,默认的 Marked 渲染器会保留引用和里面的文字。要得到带标题和图标的提示框,可以把 marked-alert 注册到 hexo-renderer-marked 提供的 marked:use。解析和 HTML 输出继续走 Hexo,站点仍能使用原有的标题锚点、文章资源映射和代码高亮。
Note
这个提示框来自文章里的普通引用语法。生成页已经带有提示框的 HTML,配色和图标的布局由样式表处理。
在原有渲染器上注册扩展
先在站点根目录确认安装了哪个 Markdown 渲染器。下面的接法针对 Hexo 8 和 hexo-renderer-marked 7。站点若使用 markdown-it 一类渲染器,要从它提供的扩展入口接入。
pnpm list hexo hexo-renderer-marked
只想先试 Alerts,可以在一个使用 Marked 的 Hexo 站点安装扩展,再把注册代码保存为 scripts/markdown-alert.js。Hexo 加载该目录时会给脚本提供 hexo 对象。
pnpm add marked-alert@2.1.2
const markedAlert = require('marked-alert');
hexo.extend.filter.register('marked:use', (use) => {
use(markedAlert());
});
保存后运行 pnpm exec hexo generate。如果文章 HTML 中已经出现 markdown-alert-tip 和标题,就可以接着检查主题 CSS 是否加载。想直接试完整的提示框样式,也可以跳到「安装本地插件包」。见 Hexo 插件文档 和 hexo-renderer-marked 扩展入口。
给哪一个钩子加逻辑
hexo-renderer-marked 会在每次渲染时准备 Marked 的扩展,再执行过滤器。已有 Marked 插件可能同时提供语法规则和 token 遍历函数,用 marked:use 可以接入完整配置。marked:extensions 则用于把单独的 tokenizer / renderer 扩展加进列表。比如只添加一种行内短码时,可以把识别和输出都留在一个扩展里。
marked:renderer 适合处理已经识别出来的节点。比如 Hexo 先识别 Markdown 图片,站点再在图片渲染函数里把本地资源路径改成公开路径。扩展提示框时,框内的图片和链接仍应交给这些函数处理。用正则直接替换整个文章字符串,既容易扫到代码围栏里的例子,也容易漏掉框内的列表和格式。见 Marked 扩展文档 和《让本地预览和公开链接共用一份资源》。
把解析、站点数据和浏览器资源分开
| 功能 | 放在哪一步 |
|---|---|
| Alerts、命名脚注、公式 | 用 marked-alert、marked-footnote 和 marked-katex-extension 在生成时输出 HTML |
折叠、带标题的 callout、::: 容器 |
块级扩展负责识别边界,内部正文继续走 Markdown 解析。折叠输出原生 <details> |
| 定义列表、Emoji | 定义列表沿用渲染器已有的简单写法,Emoji 短码由行内扩展查字典 |
| 目录、双链 | 目录复用生成后的标题 ID,双链根据 Hexo 的文章集合查找目标 |
| Mermaid | 生成页先保留代码,浏览器识别图表代码后再调用 Mermaid |
容器把边界交给 tokenizer,正文交回 Markdown
::: tip 检查生成结果 这一行提供类型和标题,结束行是单独的 :::。两行之间可以放列表、链接和围栏。本站的容器扩展会避开内部代码围栏中的 :::,防止示例内容把外层容器提前关掉。当前写法用一对三冒号界定一个容器。VitePress 用不同长度冒号嵌套容器,移植这类文件时需要单独核对嵌套规则。
带标题的 Obsidian callout 继续用引用承载正文。例如 > [!tip]- 复现细节 会生成默认收起的折叠块,首行之后的内容交回解析器。普通的五类 GitHub Alerts 由 marked-alert 处理。两种标记都要与普通引用、转义示例和代码围栏做对照。
脚注要留意主题的处理顺序。本站主题原有的脚注过滤器先处理数字脚注,新扩展能处理随后交给 Marked 的命名脚注。新站点可以选定一个脚注处理器统一负责定义、编号和回链。如果主题和插件都要处理同一种脚注,先用一处定义加两处引用测试,检查是否生成了两套脚注区,或者第二处引用无法返回。
目录使用最终标题 ID,双链使用最终文章地址
同一篇文章可以有两个「安装」标题。Hexo 会给两个标题分配不同的 ID。目录如果只取标题文字重新拼地址,两项就可能都跳到第一个标题。插件在 Markdown 解析时给 [[toc]] 留一个位置,拿到渲染后的 HTML 再用 Hexo 的 toc helper 生成列表。
双链的路径也要从渲染链获得。比如 [[my-hexo-best-practices-2|资源路径]] 的左半部分只是文章标识。插件按 url_title、标题或 slug 查找文章,找到唯一目标后使用它的实际路由。文章修改标题、使用单篇 permalink 或部署到站点子路径时,按日期和文件名猜网址容易出错。目标不存在或重名时,本站保留双链原文,便于作者定位和修改。
KaTeX 和 Mermaid 用不同的时机加载
KaTeX 在生成时输出公式 HTML,浏览器用配套 CSS 和字体排版。公式文字看得见、尺寸却不对时,可以查一下 fonts/ 下的请求,因为 katex.min.css 会从这个相对目录取字体。插件的生成器按这个布局发布文件,注入器再把样式链接放进页面。链接经过 Hexo 的 URL helper,因此也适用于 /docs/ 这样的站点子路径。
Mermaid 要处理的是图表布局。启用 Prism 时,Hexo 可能先把围栏转换成带 language-mermaid 的 HTML,图表脚本就读取代码节点里的文本。初次进入没有图表的文章时,页面只加载很小的扫描脚本。出现图表后才请求随站点发布的 Mermaid 引擎,生成的 SVG 以静态图片展示。图表解析失败时保留代码,读者仍可复制源码。见 Mermaid 用法。
本站用 Swup 在同一个页面里换正文,所以还要在 swup:page:view 后扫一次。直接打开带图表的文章和从普通文章点过去,是两条不同的验证路径。一个容易漏掉的细节是图表布局要测量文字,临时容器可以放到屏幕外,直接用 display: none 会失去参与布局的尺寸。即使浏览器成功加载了 SVG,也要确认图中节点和文字真的显示出来。
安装本地插件包
plugins/hexo-markdown-extensions/ 是本站插件的维护目录。src/index.ts 注册过滤器、生成器和资源注入,src/lib/ 放 Marked 和 Prism 的适配代码,src/browser/ 放正文样式和浏览器入口。构建后,Hexo 加载 dist/index.cjs,插件的生成器再把浏览器脚本、样式和字体复制到站点目录。包里声明了 Hexo 8、hexo-renderer-marked 7 和 Marked 15 的兼容范围。
下载 hexo-markdown-extensions-0.2.1.tgz 放到 Hexo 站点根目录,再执行以下命令。如果刚用 scripts/markdown-alert.js 做过最小验证,安装包接管 Alerts 后可以移除那段试验注册,避免重复装入同一扩展。
pnpm add ./hexo-markdown-extensions-0.2.1.tgz
pnpm exec hexo clean
pnpm exec hexo generate
维护插件时,在站点仓库运行 pnpm run build:plugins,先检查源码和类型,再用 esbuild 生成 Node 与浏览器产物。Hexo 会把包入口包装成函数并传入当前的 hexo 实例,所以 Node 产物使用 CommonJS。到插件目录运行 pnpm pack 时会先构建,再把 dist/ 装进安装包。当前站点通过 workspace:* 加载工作区里的产物,安装 .tgz 的站点也从同一个入口加载。分发验证使用独立站点和 /docs/ 子路径,核对包入口、样式地址与重点行效果。
用一篇文章核对整条渲染链
把下面这段放进一篇测试文章。它覆盖了实际提示框、命名脚注、同名标题、重点行和图表。测试代码重点行前,把站点的 syntax_highlighter 设为 prismjs,并保留 marked.headerIds: true,让标题生成可链接的 ID。
[[toc]]
## 安装 {#install}
> [!TIP]
> 先生成静态页,再从浏览器打开。确认 ==预览目录==。[^preview]
## 安装
再检查一次预览结果。[^preview]
[返回安装](#install)
```js mark:2
const host = '127.0.0.1';
const port = 4350;
console.log(host, port);
```
[^preview]: 使用独立的本地预览目录。
```mermaid
flowchart LR
Input[文章] --> Output[生成页]
```
打开生成页以后,点目录的两个「安装」,确认它们各自跳到对应的标题,再点第二处脚注和回链。看代码第二行是否有背景,并复制代码核对原文。把围栏首行换成 js{2} 后重新生成,效果应相同。用浏览器的 Network 面板核对 KaTeX 字体,切换到另一篇没有图表的文章后再点回来,看看 Mermaid 是否在切页后正常出现。
| 页面上的现象 | 检查哪里 |
|---|---|
提示框原样显示 [!TIP] |
检查实际渲染器和 marked:use 是否注册。确认标记是引用首行,示例围栏中的原文应保留 |
| HTML 有提示框类名,页面却没有样式 | 检查主题是否执行 Hexo 的资源注入,以及 CSS 请求路径是否含正确的站点前缀 |
| 公式出现,字体或间距异常 | 检查 katex.min.css 和相对路径下的字体请求 |
| 图表留在代码状态 | 先检查围栏语言和引擎请求,再检查 Mermaid 源文。切页后才出错时,检查正文更新后的初始化 |
| 双链或目录跳错 | 分别核对目标文章是否唯一,以及链接指向的标题 ID 是否真实存在 |
| 代码有语法着色,重点行却没有背景 | 先看 <pre> 上有没有 data-line="2"。有该属性时再检查 Line Highlight 的脚本请求与覆盖层位置 |
给标题和正文补上标记
正文中的 ==重点== 由 marked:use 注册的行内扩展识别,里面的文字仍交给 Marked 解析,所以 ==**重点**== 会生成带粗体的 <mark>。用反引号包住示例时,Marked 按行内代码输出字面量。这里需要写的是一小段 Marked 扩展。换成 markdown-it 时可以直接安装 @mdit/plugin-mark。见 Marked 扩展接口 和 markdown-it mark。
固定锚点接在 marked:renderer 的标题渲染处。遇到 ## 安装 {#install},渲染器用 install 生成 <h2 id="install">,只把「安装」显示在页面上。没有显式 ID 的标题仍走 Hexo 的自动锚点规则。
正文目录从生成的标题取链接,因此和手写的 [安装](#install) 用的是同一个地址。跨文章也可以用 [[markdown-extension-syntax#install-example|固定锚点示例]],效果见 固定锚点示例。
让 Prism 在已有的代码着色上标出重点行
Hexo 在 Marked 处理正文之前就把代码围栏交给高亮器。js{2} 会作为一个完整的语言名传入。js mark:2 虽然能被 Hexo 解析成行号,但 Prism 的预渲染分支省略了 data-line,浏览器就不知道应该画哪一行。围栏格式和页面效果在两个环节,需要分别接起来。见 Hexo 代码块选项 和 VitePress 行高亮。
- 生成时,在
hexo.extend.highlight里包装现有的prismjs高亮器。把js{2}拆成语言js和行号2,把 Hexo 已经解析的mark:2也转换成行号。沿用原高亮器输出着色后的 HTML,在需要时为<pre>补上data-line="2"。 - 在页面出现带
data-line的代码块时,按需加载 Prism 官方 Line Highlight。浏览器已有字体和行高信息,由插件测量并放置背景层。站点切换文章时重新查找代码块,折叠内容打开后再按实际尺寸定位。复制按钮仍读取围栏代码。
下面是 js mark:2 的实际输出。第二行有背景,三行里的 JavaScript 都沿用站点原有的 Prism 着色。见 Prism Line Highlight。
const host = '127.0.0.1';
const port = 4350;
console.log(host, port);
正文高亮需要识别行内标记,重点行需要把围栏参数交给浏览器,文件包含则需要实际读取磁盘上的文件。选插件时可以先核对扩展依赖哪一层的数据,再决定把它接进解析器、构建器还是页面。各平台的写法见《善用 Markdown 扩展语法》。