在 Hexo 中实现 Markdown 的“文件包含”:用 before_post_render 过滤器零依赖 include

本文通过 before_post_render 过滤器实现 Markdown include。➡️


在写长文档时,经常会遇到这样的需求:

  • 主文档 main.md 只做目录/总览,
  • 具体内容拆到 main1.md(记录 a)与 main2.md(记录 b)里维护,
  • 但发布到 Hexo + GitHub Pages 后,希望 main.md 页面里能直接插入两个子文件的内容。

问题在于:

  • Markdown 本身没有标准的 include 语法
  • 很多 include 方案依赖第三方插件,
  • 但在较新版本 Hexo(例如 Hexo 8.x)环境下,插件的 peer dependency 可能与 Hexo 版本冲突,导致 npm 安装失败或兼容性不确定。

本文给出一个更稳妥的方案:不依赖任何第三方插件,使用 Hexo 的 before_post_render 过滤器在渲染前“拼接文件内容”,从而实现 Markdown include。


目标效果

  • source/_posts/main.md:主文档,仅维护结构与少量说明
  • source/_posts/_fragments/main1.md:A 部分
  • source/_posts/_fragments/main2.md:B 部分
  • 生成的 main 页面中,会在指定位置插入 main1.mdmain2.md 的正文内容

目录组织(推荐)

将片段放到 _posts 下的下划线目录中,使其不被当作独立文章生成:

  • Hexo 配置文档说明:source/_posts 下任何以下划线开头的文件/文件夹会被忽略(避免被当作文章处理)。
1
2
3
4
5
6
source/
_posts/
main.md
_fragments/
main1.md
main2.md

Step 1:在 main.md 中写插入标记

source/_posts/main.md 中,用 HTML 注释写插入标记(该标记将被脚本识别并替换为目标文件内容):

1
2
3
4
5
6
7
8
9
10
---
title: main
date: 2026-01-23 10:00:00
---

这里是 main 的开头说明。

<!-- insertmd: _posts/_fragments/main1.md | --- -->

<!-- insertmd: _posts/_fragments/main2.md -->

说明:

  • insertmd: 后面跟 相对 source/ 的路径
  • 可选分隔符:| --- 表示插入后追加一段分隔内容(如水平线)
  • 标记本身是注释,不会在最终页面显示(会被脚本替换掉)

Step 2:编写 before_post_render 过滤器脚本

在 Hexo 根目录创建文件:scripts/insertmd-filter.js (若没有 scripts/ 目录则新建)

粘贴以下代码:

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
'use strict';

const fs = require('fs');
const path = require('path');

function stripQuotes(s) {
return s.replace(/^['"]|['"]$/g, '');
}

hexo.extend.filter.register('before_post_render', function (data) {
if (!data || typeof data.content !== 'string') return data;

// 语法:<!-- insertmd: path/to/file.md | --- -->
const re = /<!--\s*insertmd:\s*([^|]+?)(?:\s*\|\s*([^>]+?))?\s*-->/g;

if (!re.test(data.content)) return data;
re.lastIndex = 0;

const sourceDir = hexo.source_dir;

data.content = data.content.replace(re, (full, p1, p2) => {
const rel = stripQuotes(String(p1).trim()).replace(/\\/g, '/');
const abs = path.join(sourceDir, rel);

if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
hexo.log.warn(`[insertmd] not found: ${rel}`);
return `\n\n> [insertmd] not found: ${rel}\n\n`;
}

const text = fs.readFileSync(abs, 'utf8');
const sep = p2 ? `\n\n${stripQuotes(String(p2).trim())}\n\n` : '\n\n';

return `\n\n<!-- begin insertmd: ${rel} -->\n\n${text}\n\n<!-- end insertmd: ${rel} -->${sep}`;
});

return data;
});

工作机制简述:

  1. Hexo 在渲染文章前会触发 before_post_render
  2. 脚本扫描文章内容,匹配 insertmd 后面写的相对路径
  3. 读取对应 Markdown 文件内容,直接替换插入点
  4. 后续照常走 Markdown 渲染流程,所以片段中的标题、列表、公式(如开启 math)都能正常渲染

Step 3:编写片段文件

source/_posts/_fragments/main1.md

1
2
3
4
## A 部分(a)

- a1
- a2

source/_posts/_fragments/main2.md

1
2
3
4
## B 部分(b)

1. b1
2. b2

Step 4:本地验证与发布

本地预览:

1
2
hexo clean
hexo s

若页面正常显示插入内容,再通过 hexo d 部署到 GitHub Pages。


注意事项

  1. 片段文件不要写 Front-matter

    • 片段里不要以 --- 开头写 title/date 等,
    • 否则会被当作普通文本插入正文,页面会出现多余 YAML。
  2. 建议片段标题从 ## 开始

    • 主文章本身通常由 front-matter 的 title 作为页面主标题。
    • 片段再用 # 容易造成多个一级标题,影响 TOC 与样式。
  3. 相对路径按“最终页面位置”来检验

    因为是“插入后统一渲染”,片段里的图片/链接相对路径在最终页面中解析。建议:

    • 使用站点根路径(如 /img/...),或
    • 统一把资源放到固定目录,避免路径漂移。
  4. 文件不存在时的降级行为

    脚本找不到文件会输出警告,并在正文插入一行提示,便于快速定位路径问题。


适用场景

  • 长文拆分维护(章节/附录/读书笔记分文件)
  • 多篇文章复用同一段内容(比如统一的“实验设置”“参考实现说明”)
  • 不希望引入第三方插件、避免 npm 依赖冲突

小结

通过 Hexo 的 before_post_render 过滤器,可以用极低成本实现 Markdown include:

  • 片段分文件维护,主文档负责结构,总体渲染输出仍是一篇文章
  • 相比依赖第三方 include 插件,这种做法更稳定、可控,尤其适合 Hexo 版本较新或希望降低依赖风险的场景。 祝大家写作愉快!🚀

在 Hexo 中实现 Markdown 的“文件包含”:用 before_post_render 过滤器零依赖 include
https://nanana-szz.github.io/04-insertmd/
作者
John Doe
发布于
2026年1月23日
许可协议