frontmatter、Markdown 与 Mermaid:让写作源文件可渲染、可审查、可回滚
你提交了一篇看起来完全正常的 Markdown:编辑器能预览,标题也能折叠,Mermaid 代码块还原样显示在页面上。合并之后,CI 报一个字段类型错误;另一个站点把 article: "false" 当成真值,页面反而被公开;迁移到 MDX 后,原本只是示例的尖括号被当成组件;开发服务器能打开的链接,静态服务器却返回 404。每个现象都像“小问题”,合起来却说明写作源文件没有被当成可编译的工程输入。
这条链路可以这样理解:YAML frontmatter 提供页面身份和控制参数,Markdown 解析器把标题、段落、链接、代码块和 HTML 变成语法树,站点框架和插件把语法树转换成页面、路由、搜索数据与图示,最后构建器生成静态产物。质量门禁要在每个转换边界上留下证据,而不是只检查文件能不能被打开。
这里的关键不是记住更多语法,而是知道哪一层负责什么。CommonMark 负责基础 Markdown 的解析规则;GFM、markdown-it、MDX 和各站点框架负责扩展;Mermaid 是嵌在代码块或组件链中的另一套解析器;主题、插件和构建配置还会改变页面元数据和输出。把这些东西混成“Markdown 支持”四个字,升级和排障时就没有可用的定位轴。
页面为什么会在最后一公里失败
先看一个典型的发布现场。作者把 article: false 写在 YAML 里,编辑器只是把它当成文本,站点主题却期待真正的布尔值。YAML 解析器可以同时接受 false 和 "false",但后者属于字符串;Schema 层若没有明确类型检查,错误会继续流向导航、搜索或页面布局。类似地,order: "2203" 可能能参与字符串排序,却在数字排序中得到完全不同的顺序。
标题问题也经常被误判。很多主题会从 title 生成页面标题,正文再写一个 # 就得到两个 H1;另一些框架把正文第一个 H1 当作标题,没有 H1 反而不会报错。标题层级不是装饰,它决定目录、跳转锚点、屏幕阅读器的结构和文章之间的链接稳定性,所以团队必须先固定“标题由哪里生成”的事实,再决定正文从哪个级别开始。
Mermaid 还有一条独立的失败链:代码块语言名没有被主题识别时,它只是高亮文本;被识别后,图表源码还要经过 Mermaid 解析、主题转换和 HTML/SVG 输出。语法正确不代表图可读,图可读也不代表不含危险 HTML。正向结果应包括“产生了预期 SVG 或安全图形”,反向结果则应能说明解析失败、安全策略或尺寸限制在哪一层生效。
因此,写作门禁应把“能解析”“能构建”“能访问”“可接受”分开。check:drafts 或 YAML 解析只证明一小段链路;标题、链接、图示、客户端 hydration、移动端布局和发布路径需要更高层的证据。
把 frontmatter 当成页面契约
frontmatter 必须位于文件最前面,用成对的 --- 包围有效 YAML。VitePress 使用 gray-matter 解析 Markdown 文件顶部的 YAML,并允许页面字段覆盖站点或主题配置;Docusaurus 的内容插件也把 frontmatter 当作页面元数据,并为文档规定自己的字段;VuePress 则通过页面 frontmatter 和插件把元数据送入主题与构建流程。字段名相同不代表语义相同,真正的 Schema 属于“框架 + 主题 + 插件 + 项目脚本”这个组合。
可以把页面契约分成三层。第一层是跨站点常见、但仍需各自声明的内容元数据,例如 title、description、tags。第二层是框架或主题字段,例如 VitePress 的 layout、head,Docusaurus 文档的 id、slug、sidebar_position,以及某些 VuePress 博客主题提供的 article、timeline、pageInfo。VuePress 核心只定义自己的 frontmatter 字段,默认主题和第三方主题各有额外契约,不能把本站主题字段写成 VuePress 2 通用 API。第三层是团队自己的字段,例如 status、owner、risk 或 reviewers。第三层不能因为“YAML 能解析”就自动参与发布,必须声明谁读取、允许哪些值、错误时怎样阻断。
这是一个适合知识站点文章的契约样例。这里的 category、status、order 是否生效,取决于项目主题和脚本;它们不能被误认为 CommonMark 或三个框架的共同标准。
---
title: "frontmatter、Markdown 与 Mermaid"
description: "解释元数据、正文解析和图示安全之间的工程边界。"
article: true
status: "published"
order: 2203
---字段值的类型要故意写得保守。布尔字段只接受 true 或 false,不接受 "true";顺序字段只接受整数;枚举字段只接受团队约定的几个字符串;列表字段即使只有一个元素也保持数组形状。字符串中的冒号、井号、花括号和前导零可能触发 YAML 的特殊解析,标题、URL、版本标识和以数字开头的标签最好加引号。密码、令牌和带查询参数的私密 URL 不应放在 frontmatter,因为它会进入页面数据、搜索索引或构建缓存。
一个实用 Schema 至少检查必填字段、类型、枚举、额外字段和互斥关系。例如,cover 与正文里的同一张海报不能同时声明,draft: true 不应与 status: "published" 并存,id 和 slug 需要满足路由唯一性。不要同时维护语义重复的 tag 和 tags 来“兼容所有主题”;兼容应该通过解析阶段的显式映射完成,然后只保留一个规范字段。
下面的 JSON Schema 只对应前面的五个样例字段,目的是让 YAML 解析后的类型进入可执行契约。真实站点要把主题和插件实际读取的字段补全后再启用 additionalProperties: false,否则 Schema 自己会把合法主题字段误报成错误。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["title", "description", "article", "status", "order"],
"properties": {
"title": { "type": "string", "minLength": 1 },
"description": { "type": "string", "minLength": 1 },
"article": { "type": "boolean" },
"status": { "enum": ["draft", "published", "archived"] },
"order": { "type": "integer", "minimum": 0 }
},
"additionalProperties": false
}校验器必须先用框架一致的 YAML parser 得到对象,再把对象交给 JSON Schema;若直接用正则读取 key: value,引号、数组、多行字符串和注释很快就会让类型判断失真。Ajv 一类验证器只负责对象契约,路由唯一性、字段互斥和跨文件重复仍应作为额外规则执行。
安装、初始化与 Node 支持线
安装站点工具时先看项目锁文件、目标包的 engines 和框架支持线,再选择包管理器。VuePress 的入门页仍列出 Node.js 20.9.0 或更高版本,同时提醒 VuePress 2 处于 RC;但当前 vuepress@next 的官方包清单已经要求 Node.js 22.18.0 或更高版本。安装器会按包清单判定兼容性,因此升级前应执行 npm view vuepress@next engines peerDependencies,并以目标版本实际声明为准。已有项目应沿用锁定的核心、bundler、主题和插件组合,不要在内容分支上无意刷新整棵依赖树。
VitePress 官方入门页要求 Node.js 22 或更高版本,并明确说明 VitePress 是 ESM-only:配置不能简单照搬 CommonJS 的 require() 写法;VitePress 安装说明还给出了 package.json 的 type: "module" 或 .mjs、.mts 文件边界。Docusaurus 官方安装页要求 Node.js 20 或更高版本,脚手架通过 create-docusaurus 生成站点;升级时所有 @docusaurus/* 包要保持同一版本线,详情见Docusaurus 安装与升级说明。Node 版本并不是可有可无的开发机偏好,它会改变 ESM、依赖解析、构建内存和插件可加载性。
新站点可以用下面的最小初始化动作建立可追踪基线。命令中的版本标签只适合试验;团队 CI 应改成锁文件和经过评审的固定版本。
# 先确认 Node 与包管理器,不要让不同成员自动选择不同运行时
node --version
npm --version
# VitePress 试验入口;官方当前示例使用 next 标签,正式项目应锁定版本
npm add -D vitepress@next
npx vitepress init
# Docusaurus 新建站点入口
npx create-docusaurus@latest docs-site classic
# VuePress 2 本地项目入口,继续使用项目已有 bundler 与主题组合
npm view vuepress@next engines peerDependencies
npm add -D vuepress@next @vuepress/bundler-vite@next @vuepress/theme-default@next vue初始化后马上做三件事:记录 node --version、包管理器版本和 lockfile;执行一次空站点开发预览;执行一次生产构建并保存构建目录中的关键文件名。若机器只有 Node 20 而项目选择当前 VitePress,安装可能在依赖阶段就失败;若 VuePress 2 RC 升级后拉入不兼容主题,常见证据是 peer dependency、配置导出或插件 hook 的类型/运行时错误。VuePress 核心、bundler、主题和插件的 RC 尾号可能不同,不能强求版本字符串完全一致,应检查各包 peer dependency 并用干净构建证明组合可用。更换框架前先把失败归类为运行时版本、模块格式、主题 API 或正文语义,不要用删除 lockfile 的方式“修复”。
配置字段怎样改变页面行为
站点配置和页面 frontmatter 是两层覆盖关系。站点配置通常确定默认主题、解析器、路由、搜索、Mermaid 或 Markdown 插件;页面 frontmatter 只覆盖当前页面允许覆盖的字段。VitePress 的 title、description、head、layout 等有官方 frontmatter 定义,且页面数据可通过 $frontmatter 或 useData() 读取,见Frontmatter 配置参考。VuePress 的 markdown 配置可以调整 markdown-it、标题、链接、frontmatter、组件和代码块插件,见VuePress Markdown 配置。
Docusaurus 的 markdown 配置则是一个很清晰的例子:format 决定使用 MDX、CommonMark 或按扩展名检测,mermaid 决定是否识别 Mermaid 代码块,parseFrontMatter 可包裹默认解析器,hooks.onBrokenMarkdownLinks 和 hooks.onBrokenMarkdownImages 可以改变坏链接与坏图片的处理级别。Docusaurus 配置参考还说明,旧的顶层 onBrokenMarkdownLinks 已弃用,应迁移到 markdown.hooks;生产构建默认对普通坏链接报错,而 Markdown 链接和图片各有自己的默认严重级别。配置项不是“开关清单”,每个字段都会改变输入、输出或失败策略。
项目配置应尽量使用类型提示和集中定义,避免把同一含义分散到主题、页面和插件中。一个可迁移的配置形状如下:
// .vitepress/config.ts 或等价的站点配置文件
import { defineConfig } from 'vitepress'
export default defineConfig({
title: 'Knowledge Site',
description: 'Versioned engineering documentation',
base: '/',
markdown: {
anchor: { level: [2, 3, 4] },
headers: { level: [2, 3] },
config(md) {
// 只接入经过审查并锁定版本的 Markdown 插件
// md.use(approvedPlugin)
},
},
})这里的 base 会影响根路径链接和静态发布位置,属于图片与公开路径专题会继续处理的交付约束;markdown.anchor 会影响标题生成的锚点,headers 会影响目录或运行时页头数据,config(md) 会把第三方代码放进构建过程。VitePress 官方说明其 Markdown 渲染器是 markdown-it,并允许通过 markdown.config 接入插件;这也意味着一个插件可能改变标题、HTML、链接或安全边界,不能只看它是否“有用”。
VuePress 插件有相似但不相同的生命周期。官方 Plugin API 将 hook 分为初始化、准备文件、开发/构建阶段,例如 extendsMarkdown、onPrepared、extendsBundlerOptions 和 onGenerated;VuePress Plugin API说明了这些入口。Docusaurus 插件则可以通过 loadContent、contentLoaded、createData、addRoute 等生命周期参与内容和路由生成,官方插件文档明确提醒插件同时影响开发和构建。插件能力越强,读取内容、修改产物和引入依赖的权限越大,必须纳入供应链审查。
Markdown 的稳定写法:标题、链接与代码块
基础 Markdown 语法很小,但站点工程依赖的是“方言边界”。CommonMark 规范定义了 ATX 标题、Setext 标题、围栏代码、链接、图片和 HTML 等基础块;GFM 规范在此基础上增加表格等扩展。VuePress 和 VitePress 都基于 markdown-it 扩展,Docusaurus 还要区分 Markdown 与 MDX。编辑器的预览器只代表自己的解析器,不能作为站点输出的唯一证据。
本项目类知识文章让 frontmatter 的 title 负责页面标题,正文从 ## 开始,避免主题标题与正文 H1 重复。对普通文章,标题层级应保持连续:## 是章节,### 是该章节下的操作或机制,不能为了字体大小跳到 #### 或用加粗代替标题。标题文本尽量稳定、短而有语义;同一页不要重复相同标题,中文标点、斜杠和括号可能在不同 slugify 实现中生成不同锚点。
链接要区分三种对象:源码文件链接、站点路由链接和外部资料链接。源码协作最适合相对 .md 链接,编辑器和死链工具能直接找到目标;站点内部路由要遵循当前框架的 base 与页面后缀规则;外部链接要写出访问目的,不要把一串裸 URL 堆在文章末尾。VuePress 官方建议内部 Markdown 链接优先使用相对路径;VitePress 允许省略 .md 或使用路由形态,但跨框架迁移时省略后缀可能增加检查器适配成本,参考VitePress Markdown 扩展。
代码块要声明语言,并把“示例文本”和“可执行命令”分开。命令里的 your-project、<access-key> 和 localhost 是安全占位符;不要把真实 Cookie、Token 或内部主机名放进注释,因为代码块会进入搜索、复制和构建缓存。shell 示例还要说明 POSIX shell 与 PowerShell 的差异,不能要求读者把反斜杠续行原样粘贴到 Windows 终端。
## 连接证据应该怎样保存
用相对链接回到同一站点的源文件:[框架配置](./vuepress.md)。
外部规范就地说明用途:[CommonMark 的围栏代码规则](https://spec.commonmark.org/spec/#fenced-code-blocks)。
```bash
node --version
npm run docs:build
```当代码块内部还要展示 Markdown 围栏时,外层使用四个或更多反引号,或者改用波浪线围栏。否则解析器会提前结束代码块,后半段可能被当作正文、HTML 或 MDX。这个错误常在文章中插入“配置文件示例”时出现,静态构建未必立即报错,却会破坏标题和链接结构。
Mermaid 启用链与安全默认值
Mermaid 不会因为项目安装了 mermaid 包就自动生效。首先要确认站点框架有没有把 mermaid 语言代码块交给 Mermaid;其次要确认主题或组件把图渲染到页面;最后才是图的语法、字体、主题、尺寸和安全级别。Docusaurus 有官方 @docusaurus/theme-mermaid,安装主题并在配置中设置 markdown.mermaid: true 才会处理 Mermaid 代码块,见官方 Mermaid 主题文档。
DOCUSAURUS_VERSION=$(node -p "require('./node_modules/@docusaurus/core/package.json').version")
npm install --save-exact "@docusaurus/theme-mermaid@$DOCUSAURUS_VERSION"import type { Config } from '@docusaurus/types'
export default {
themes: ['@docusaurus/theme-mermaid'],
markdown: {
mermaid: true,
},
} satisfies Config安装时让 @docusaurus/theme-mermaid 与其他 @docusaurus/* 包保持同一版本,再运行生产构建;只在开发服务器里看到图,仍不能证明 SSR 产物和发布路径正确。
VitePress 官方文档提供了 markdown-it 的扩展入口,但没有把 Mermaid 当作核心 Markdown 开关;VuePress 官方配置和 Plugin API 也提供了 Markdown 扩展与生命周期入口。两者接入 Mermaid 时要使用与当前框架、markdown-it 版本和 SSR/构建方式匹配的已审查桥接方案,不能把 Docusaurus 的 themes 配置复制过去。当前站点如果只安装了 Mermaid 依赖,最小反例就是“代码块仍然显示为 mermaid 高亮文本”;这证明依赖存在,不证明渲染链建立。
Mermaid 配置 Schema 将 securityLevel 的默认值定义为 strict。strict 会编码文本中的 HTML 并关闭点击功能;loose 允许 HTML 和点击;antiscript 只移除 script 元素,同时仍允许其他 HTML 和点击;sandbox 在 sandboxed iframe 中渲染,会限制脚本、弹窗和跨页链接等交互,参见securityLevel 配置说明。公开知识站点通常从 strict 开始;antiscript 不是通用 HTML 消毒器,不能当成 strict 的等价替代。
// 由站点的 Mermaid 适配层调用,具体入口随框架插件而定
const safeMermaidConfig = {
startOnLoad: false,
securityLevel: 'strict',
theme: 'base',
maxTextSize: 2000,
maxEdges: 100,
}
mermaid.initialize(safeMermaidConfig)maxTextSize 限制图源码长度,maxEdges 限制边数;示例中的 2000 和 100 是演示值,生产值应由现有图复杂度、构建资源预算和拒绝测试决定。节点数、布局耗时、并发和最终 SVG 大小仍需由适配层补充限制。Mermaid 默认把 securityLevel、startOnLoad、maxTextSize、maxEdges 等放在 secure 配置列表中,只允许站点通过 mermaid.initialize() 改写,图作者不能用单图配置覆盖。不要为了“允许主题定制”替换成更短的 secure 列表;旧的 %%{init}%% 指令已弃用,新内容应使用图源码自己的 Mermaid frontmatter做允许的展示配置,而安全上限继续由宿主持有。这里的 Mermaid frontmatter 位于图源码内部,不是站点页面顶部的 frontmatter。
Mermaid 的安全配置不能只靠读者自觉。对外部投稿、CMS 导入、自动生成文档和包含用户输入的页面,应默认拒绝 HTML、点击回调、脚本和未经批准的链接;在送入 Mermaid 前限制字符、控制字符和协议,在渲染器外限制节点、超时、并发与输出体积。允许样例要证明普通节点和连线可渲染;拒绝样例要分别包含 HTML 标签、click 回调、超长文本和超量边,并断言输出中没有可执行标签或事件属性、点击没有生效、超限图没有进入发布产物。mermaid.parse() 只能证明语法可解析,不能替代 SVG/DOM 安全检查。图中的文字还可能包含隐私、路径或业务标识,安全渲染不等于内容脱敏。
图形较复杂时,可以在隔离的构建步骤中生成 SVG,再用允许列表型 SVG 清理器移除脚本、事件属性、foreignObject 和不受信任链接;不能因为渲染发生在 CI 就把原始 SVG 当成安全产物。采用客户端渲染时,则要把 Mermaid bundle、浏览器 CPU、CSP 和运行时错误一起纳入容量与安全评估。
正向实验:从 YAML 到页面证据
正向实验先使用一个最小页面,故意只放站点真正需要的字段、二级标题、相对链接、语言标注的代码块和一个简单流程图。实验不应直接在正式文章上改坏内容,而应放进临时 fixture,并使用与生产构建相同的 Node、锁文件和主题配置。
---
title: "渲染链路样例"
description: "验证 frontmatter、标题、链接、代码和 Mermaid。"
article: true
status: "draft"
order: 9900
---
## 一个可定位的页面
链接回源文件:[入口页](./README.md)。
```json
{"kind":"render-check","safe":true}
```
```mermaid
flowchart LR
Source["Markdown source"] --> Parse["Parser"]
Parse --> Gate["Quality gates"]
Gate --> Page["Static page"]
```预期证据有四层。第一,YAML 解析得到布尔、数字和数组,而不是所有值都变成字符串;第二,标题检查只看到连续的 ## 和 ###,没有重复 H1;第三,链接检查能找到 README.md 对应的源或路由;第四,构建产物中出现页面 HTML,页面中出现 Mermaid 输出而不是原始代码块。浏览器打开时还要确认 SVG 没有横向溢出、节点文字可读、无 script 标签,移动视口没有把图挤成不可理解的一条线。
本仓库已有的最小站点检查可以这样串起来。check:drafts 负责项目已有的草稿和文章约束,标题层级、类型、链接和构建检查按仓库实际脚本执行;下面的命令不替代浏览器验证。
npm run check:drafts
npm run test:heading-hierarchy
npm run typecheck
npm run build
# 生成后只检查关键证据,不把“退出码为 0”当成页面正确
Test-Path dist/index.html
rg -n "渲染链路样例|render-check|flowchart" dist如果使用 Docusaurus,还应检查 build/ 中目标路由、生成的静态 HTML 和页面脚本;如果使用 VitePress,则检查 .vitepress/dist/。构建目录名称是框架默认值,不应被团队脚本硬编码成所有框架都一样。实际发布使用子路径时,用真实 base 打开页面,确认相对链接和 Mermaid 资源仍能加载。
反向实验:让错误留下可归因证据
反向实验一次只改变一个因素,否则失败日志无法说明原因。第一轮把 article: true 改成 article: "true",Schema 应拒绝类型;第二轮删掉 title 或把 order 改成带小数的字符串,Schema 应指出必填或类型错误;第三轮在正文加入第二个 #,标题门禁应指出重复 H1;第四轮把相对链接改成不存在的文件,死链检查应给出源文件和目标;第五轮把 Mermaid 图改成未闭合的节点或错误的 diagram type,渲染器应返回非零结果或页面错误占位。
---
-article: true
-order: 9900
+article: "true"
+order: "9900"
---
# 这会引入一个额外 H1
-[入口页](./README.md)
+[不存在的页面](./missing-page.md)故障证据要按层保存。YAML 解析成功但 Schema 失败,说明问题在类型契约;Schema 通过但构建失败,说明问题在框架字段、插件或正文方言;构建成功但浏览器的 Mermaid 是原始代码,说明语言块没有接入 Mermaid 适配层;浏览器能看到图但不安全扫描通过不了,说明安全配置或 SVG 清理有问题;页面正常但旧 URL 404,说明路由迁移和重定向没有同步。不要把所有失败都归结为“缓存”,也不要用提高重试次数掩盖可重复的解析错误。
可以把反向实验做成 CI 的最小矩阵:一个无效类型 fixture、一个重复 H1 fixture、一个死链 fixture、一个 Mermaid 语法 fixture 和一个包含 <script> 文本的安全 fixture。每个 fixture 都要断言“检查失败”,并检查错误中包含文件名、字段或行号。这样升级解析器时,错误消息文字变化不会误判成功,真正重要的失败类别仍然可见。
组件混写与不可信内容
VuePress 和 VitePress 的 Markdown 会进入 Vue 组件编译链,VitePress 官方明确说明每个 Markdown 页面最终会作为 Vue Single-File Component 处理;Using Vue in Markdown也提醒 SSR 页面中的浏览器 API 只能在客户端生命周期中使用。Docusaurus v3 默认把 .md 和 .mdx 都按 MDX 处理,但官方建议使用 JSX、import 或 export 时采用 .mdx,并逐步移除旧 MDX 兼容写法。
这带来两个容易混淆的边界。围栏里的 const token = "<secret>" 是代码文本;围栏外的 <SomeComponent />、import 和表达式可能成为可执行组件输入。组件本身不是“安全 HTML”,它可以读取构建数据、访问浏览器 API、改变路由或发出网络请求。站点应维护允许组件清单,限制组件 props 的来源,禁止把作者输入直接拼成模板或 v-html/危险 HTML,构建时对 HTML、SVG、脚本和外链做扫描。
Mermaid 也属于不可信输入面。节点文字来自文章作者时,严格模式通常足够;节点文字来自 CMS、Issue、用户评论或自动生成器时,必须在进入 Mermaid 前做长度、字符、链接和控制字符限制。loose 或 antiscript 可能让点击和 HTML 重新成为攻击面;sandbox 可以缩小脚本影响,但会损失部分交互,不应被当作“所有问题自动消失”。安全配置改变后需要做允许/拒绝两组测试,不能只看一张图还能否显示。
第三方插件还会在 Node 构建进程中运行。它们可能读取整个内容目录、环境变量、缓存和静态资产,也可能通过远程请求扩大供应链边界。CI 使用最小权限的构建身份,默认不向文档构建暴露生产密钥;构建日志和 source map 中不得出现本机绝对路径、内部域名、真实账号或令牌。需要处理不可信内容时,优先在隔离容器中构建,限制网络、文件读取、CPU、内存、超时和输出体积。
把检查接进项目与清理回滚
项目接入不能只在文章目录里放一个示例。先选一份真实页面作为 fixture,再把检查分成快速反馈和发布门禁两级。快速反馈在编辑器保存或 pre-commit 时检查 frontmatter、标题和 Markdown 语法;发布门禁在干净依赖环境中检查所有文章、死链、图片、Mermaid、构建产物、关键路由和移动端页面。外部链接可以采用重试和隔离网络策略,但内部链接、图片和路由不应因为网络不稳定而被放过。
一个不依赖具体框架的门禁组合可以这样安排:
# Markdown 结构;这一步不会校验 frontmatter 字段类型
npx markdownlint-cli2 "docs/**/*.md" "!.cache/**"
# 项目应把 YAML parser + JSON Schema 封装成独立脚本
npm run check:frontmatter-schema
# 外部链接单独执行,避免把临时网络抖动混入源文件错误
npx markdown-link-check docs/article/example.md
# 框架级证据:按实际项目脚本替换
npm run check:drafts
npm run test:heading-hierarchy
npm run build如果项目已经有自己的检查脚本,应优先让它输出统一的文件、字段、行号和失败类别;不要为了“多一个工具”把同一规则复制成三份。Schema 检查可以采用 JSON Schema + Ajv、项目已有的 YAML parser,或站点框架暴露的 frontmatter hook。死链工具要理解 .md 到生成路由的映射,不能只把源码链接当成普通文件系统路径;标题检查要忽略代码块和 frontmatter,不能用一个简单的 ^# 正则误报示例。
清理动作也要纳入项目接入。删除或改名页面时,保留旧 URL 清单,决定永久重定向、临时兼容还是明确下线;删除草稿时同时清掉搜索索引、构建缓存、预览服务和 CDN 上的旧产物。Mermaid 适配层被移除时,先把图转成受控 SVG 或保留可再生成的 .mmd 源,不要只留下截图。插件被替换时,先保留旧 lockfile 和上一个可部署构建,再验证新插件的标题、HTML、路由、搜索和图示输出。
回滚应回答三个问题:源文件恢复到哪个版本,依赖和配置是否同样恢复,外部缓存和索引如何清理。只回滚 Markdown 不一定能恢复页面,因为新主题可能仍按新字段解析;只回滚依赖也不一定能恢复路由,因为文件名或 slug 已经变化。最稳妥的交付物是“源提交 + lockfile + 构建配置摘要 + 静态产物哈希 + 旧 URL 映射”的成组版本。恢复演练至少访问一篇文章、一张图片、一张 Mermaid 图和一个旧链接。
迁移时保留兼容窗口
Markdown 的迁移成本通常不在正文段落,而在元数据、组件、锚点、路由和插件语义。迁移前先生成字段使用清单和页面路由清单,再为每个字段标注来源、消费者、默认值和替代字段。新解析器先以兼容模式读取旧字段,同时生成规范字段;新旧页面并行构建并做 HTML、标题、链接、图示和关键截图差异;确认新构建稳定后,才切换导航和搜索。
Docusaurus v2 到 v3 是一个有代表性的迁移案例:v3 迁移说明指出主要变化来自 MDX v1 到 MDX v3,解析更严格,部分内容会编译失败或产生不同输出;mdx1Compat 用于过渡旧注释、admonition 和标题 ID 写法,不应成为永久依赖。Docusaurus 3 默认仍按 MDX 解释 .md 和 .mdx;选择 markdown.format: 'detect' 后,.md 才按 CommonMark、.mdx 按 MDX。使用 JSX、import 或 export 的文件应改用 .mdx,并分别测试 CommonMark 与 MDX fixture。这意味着“文件后缀不变”不代表语义不变。
VitePress 的 ESM-only 约束会让配置入口、插件导入和 Node 版本同时改变;VuePress 2 RC 的配置/API 稳定性则要求每次升级阅读变更记录。迁移时不要把 require、module.exports、Vue 组件、MDX JSX 和 Mermaid 配置混在一个“兼容开关”里。先按模块格式、frontmatter 字段、标题锚点、组件语法和图示适配层分别验证,失败时更容易回滚。
标题 ID 是常见的隐性兼容问题。VitePress 支持在标题后使用 {#custom-id},但 Docusaurus 新版 MDX 对类似语法的处理不同,旧兼容选项也可能在将来被关闭。跨框架内容优先使用简单标题和自动锚点;确实需要稳定外链时,使用框架支持的显式 ID,并为目标框架写单独测试。重命名标题前先搜索仓库和外部文档中的锚点引用,避免页面正文没有变化,链接却静默失效。
权限、容量、成本与长期治理
frontmatter 和 Markdown 看似是文本,实际会进入一条高权限的构建流水线。构建身份至少需要读取文档源、插件和模板,通常不需要读取生产密钥、生产数据库或无关仓库。CI 的环境变量分成构建必需、可选遥测和禁止暴露三类;文章样例统一使用占位符,日志、截图、Mermaid 节点和 HTML 属性都要扫描真实凭证、账号、内网地址和客户数据。
容量问题有三层。第一层是源文件和搜索索引大小,过大的代码块、重复 include 和高基数 frontmatter 会放大构建与检索;第二层是 Mermaid 和组件的 CPU、内存、浏览器执行时间,复杂图在本机能渲染不代表 CI 并发时能稳定完成;第三层是静态产物、缓存、CDN 和历史构建保留,旧内容可能继续可访问。为 Mermaid 设置文本长度和图复杂度上限,为构建设置内存和超时观测,为索引设置文档数量、字段和保留策略,才能把“站点变慢”还原成可测量的资源问题。
成本不仅是托管费用,还包括 CI 分钟、浏览器视觉回归、外部死链探测、搜索服务调用、第三方插件升级和人工迁移时间。客户端 Mermaid 往往把图形解析成本转嫁给访问者;构建期生成 SVG 会增加构建成本和资产存储,但能减少首屏 JavaScript 和运行时攻击面。团队应根据页面数量、更新频率、访问设备、图示交互需求和维护能力选择,不要把某种渲染形态写成永远正确。
长期治理可以落成一个小而稳定的台账:框架与 Node 支持线、lockfile、主题和插件 owner、frontmatter Schema 版本、废弃字段、Mermaid 安全策略、链接与标题门禁、构建产物保留期、旧路由清单、上一次可部署版本和退出步骤。每次依赖升级都用固定样例做正反渲染;每次字段迁移都做双读、双构建或重定向验证;每次插件新增都记录它能读取什么、能修改什么、是否需要网络。
团队规范最终要让错误尽早暴露:作者在提交前看到字段类型错误,评审者能看到标题和链接差异,CI 能阻断不安全图示和缺失路由,发布者能拿到可恢复产物,维护者能在废弃字段真正删除前找到所有消费者。做到这些,Markdown 仍然保持易写,但它已经不再是无法验证的自由文本,而是拥有身份、结构、渲染权限、兼容窗口和回滚证据的工程源文件。
