Markdown、MDX 与 AsciiDoc 文档工程工具手册
预览正常,合并后却是一页坏文档
接口手册在作者的编辑器里标题、代码和图片都正常,合并后站点却出现重复锚点;另一位同事把同一文件交给 Pandoc,表格和提示框又丢了。更危险的情况发生在 MDX:评审者以为自己只是在看 Markdown,构建器却执行了文档里的 JavaScript。到了 AsciiDoc,include::[] 又可能读取工作区之外的文件。
这些故障通常不是“语法没学会”,而是仓库没有回答四个工程问题:文件按哪种方言解析,谁有权执行扩展,哪条命令代表最终产物,什么证据才能让变更进入主分支。
Markdown、MDX 与 AsciiDoc 都是纯文本源文件,但运行模型不同:
| 格式 | 构建时发生什么 | 最适合的工程现场 | 首要风险 |
|---|---|---|---|
| Markdown | 解析器把标记转换成 AST 或 HTML | README、变更记录、短篇知识库、代码邻近文档 | 方言和插件不同导致渲染漂移 |
| MDX | Markdown、JSX 和 ESM 被编译成 JavaScript | 需要受控交互组件的产品文档和组件文档 | 文档作者获得代码执行能力 |
| AsciiDoc | 预处理器先处理属性、条件和 include,再构建文档模型并导出 | 长篇手册、模块化书籍、多目标 HTML/PDF | include、扩展和远程资源越界 |
CommonMark 当前稳定规范线是 0.31.2,它提供可测试的 Markdown 基线,但 GitHub Flavored Markdown、各站点生成器和编辑器仍会增加表格、任务列表、脚注、容器、组件等扩展。格式选择不能只写“使用 Markdown”,还要固定解析器、扩展集合和最终构建命令。规范与测试样例可从 CommonMark Spec 获取。
先用内容寿命和执行权限选格式
一份只包含标题、段落、链接和代码块的服务 README,用 CommonMark 或仓库既有 Markdown 方言最稳。它依赖少、Git diff 清晰,迁移到其他站点时损失也最小。
当文档必须嵌入 <ApiPlayground />、<Chart /> 或框架组件时,MDX 能把内容与交互放在一起。代价是构建链必须拥有 Node.js、JSX runtime、打包器和组件依赖,文档评审也必须升级为代码评审。MDX 官方明确把 MDX 视为编程语言;不可信作者提交的 MDX 不能直接编译执行,安全说明见 MDX Getting started。
当手册需要章节 include、条件发布、交叉引用、脚注、索引、长表格和稳定 PDF 时,AsciiDoc 的结构表达更完整。它更适合“一个内容模型导出多个正式制品”,但团队要接受 Ruby/JVM/JavaScript 处理器、主题和扩展的维护成本。
可以用下面三个问题完成决策:
内容作者是否都拥有仓库代码执行权限?答案是否定的,就不要让投稿入口直接生成或执行 MDX。文档是否依赖大量复用章节、条件变体和正式 PDF?答案是肯定的,AsciiDoc 通常比堆叠 Markdown 插件更稳定。文档是否要在多个平台间自由搬迁?答案是肯定的,应优先使用 CommonMark 交集,把平台扩展隔离在少数文件中。
格式不是身份标签。一个仓库可以让 README 使用 Markdown、交互式组件文档使用 MDX、交付手册使用 AsciiDoc;但同一类内容应只有一个权威源,不能长期同时维护 .md、.mdx 和 .adoc 三份副本。
把工具锁进项目,而不是锁进某台电脑
Markdown 和 MDX 工具链使用受支持的 Node.js LTS 发行线,并通过项目包管理器和 lockfile 固定依赖。MDX 3 的 @mdx-js/* 包是 ESM-only,官方最低要求是 Node.js 16;新项目应选择仍受支持的 Node.js LTS,而不是把最低兼容版本当成推荐版本。核心编译器与打包器集成见 @mdx-js/mdx 和 MDX integrations。
在已有 Node 项目中启用一条可审计的文档链:
npm install --save-dev @mdx-js/mdx@^3 markdownlint-cli2
npm ls @mdx-js/mdx markdownlint-cli2lychee 是独立的 Rust CLI,不是 Node 构建依赖。开发机按 lychee installation 选择系统包、预编译二进制或 cargo install lychee --locked;CI 使用固定发行版本或固定镜像 digest,并在启动时输出 lychee --version。
package.json 声明允许的版本范围,package-lock.json 记录实际解析结果。CI 使用 npm ci,不使用会重写 lockfile 的 npm install。若项目使用 Vite/Rollup、webpack/Next.js 或 esbuild,应分别安装 @mdx-js/rollup、@mdx-js/loader 或 @mdx-js/esbuild,不要同时启用多套编译入口。
AsciiDoc 最小链路建议用 Bundler 隔离到项目目录。Windows 可以先安装 Ruby,再使用同一份 Gemfile;macOS 和 Linux 也不要依赖系统预装 gem:
source "https://rubygems.org"
gem "asciidoctor", "~> 2.0"
gem "asciidoctor-pdf", "~> 2.3"bundle config set path vendor/bundle
bundle install
bundle exec asciidoctor --version
bundle exec asciidoctor-pdf --versionAsciidoctor 官方把 Bundler 作为工作区隔离的首选方式,也提供系统包、Homebrew、gem install、Docker、AsciidoctorJ 和 Asciidoctor.js 入口,差异见 Asciidoctor installation。直接生成 PDF 需要单独的 asciidoctor-pdf gem;Asciidoctor PDF 1.x 已结束维护,新项目使用 2.x 线,安装与支持条件见 Asciidoctor PDF installation。
机器上不能安装 Ruby 时,可以用固定 digest 的容器镜像运行构建,但要把工作区只读挂载,把输出目录单独设为可写,并确认镜像许可、来源和漏洞扫描策略。latest 只能用于临时试跑,不能作为 CI 基线。
编辑器预览只负责缩短反馈,不负责签发产物
VS Code 内置 Markdown 预览,打开 .md 后使用“Markdown: Open Preview to the Side”。预览安全级别默认应保持 Strict:它禁用脚本并阻止不安全资源;安全设置对整个工作区生效,行为说明见 VS Code Markdown preview security。
MDX 可以安装 MDX 官方推荐的编辑器支持,例如 VS Code 的 mdx-analyzer。编辑器扩展有读取工作区、运行语言服务或启动进程的能力,应固定扩展 ID,由仓库推荐列表启用,并在升级前看权限和发布者。预览中组件可见,不代表生产打包器、别名、CSS 或服务端渲染也一致。
AsciiDoc 在 VS Code 中可安装 asciidoctor.asciidoctor-vscode。桌面版支持预览、include、HTML/PDF/DocBook 导出等能力,Web 版不具备全部导出能力,安装和能力矩阵见 AsciiDoc extension installation。扩展预览应使用 SAFE 或更严格模式,不自动加载仓库提供的 Ruby 扩展。
团队应把“编辑器看到的效果”降级为即时反馈,把下面的项目命令作为权威入口:
{
"scripts": {
"docs:lint": "markdownlint-cli2 \"docs/**/*.md\"",
"docs:links": "lychee --config lychee.toml \"docs/**/*.{md,mdx,adoc}\"",
"docs:mdx": "node scripts/check-mdx.mjs",
"docs:adoc": "bundle exec asciidoctor --safe-mode=safe --failure-level=WARN -B docs -D build/html docs/index.adoc",
"docs:pdf": "bundle exec asciidoctor-pdf --safe-mode=safe --failure-level=WARN -B docs -D build/pdf docs/index.adoc"
}
}字段会直接改变构建边界:
--safe-mode=safe 限制 AsciiDoc 对工作区之外路径的访问;Asciidoctor CLI 默认其实是 UNSAFE,不能省略。--failure-level=WARN 让缺失 include、非法样式等警告导致非零退出,否则工具可能生成一份已知错误的 HTML。-B docs 把 include 和资源解析根固定在 docs,避免作者机器当前目录改变结果。
-D build/html 和 -D build/pdf 把生成物移出源目录,防止二进制 PDF 和 HTML 噪声混进源文件 diff。
正向实验:让三种源文件产生可检查的结果
建立一个临时实验目录:
doc-lab/
├── docs/
│ ├── guide.md
│ ├── component.mdx
│ ├── handbook.adoc
│ └── partials/
│ └── install.adoc
├── scripts/
│ └── compile-mdx.mjs
├── Gemfile
└── package.jsondocs/guide.md 只使用可移植结构,并为代码块声明语言:
# Cache client
Use the client with an explicit timeout.
```js
const timeoutMs = 1000
```
See [operations](./handbook.adoc).docs/component.mdx 加入一个不访问网络、文件系统和环境变量的纯展示组件:
export function Status({kind, children}) {
return <strong data-kind={kind}>{children}</strong>
}
# Build state
<Status kind="ok">ready</Status>scripts/compile-mdx.mjs 编译而不执行文档:
import {mkdir, readFile, writeFile} from 'node:fs/promises'
import {compile} from '@mdx-js/mdx'
const source = await readFile('docs/component.mdx')
const result = await compile({path: 'docs/component.mdx', value: source}, {
development: false,
outputFormat: 'program'
})
await mkdir('build/mdx', {recursive: true})
await writeFile('build/mdx/component.mjs', String(result))
console.log('compiled docs/component.mdx -> build/mdx/component.mjs')成功时输出目录中会出现 ESM 模块,内容包含 react/jsx-runtime 导入、Status 函数和默认的 MDXContent 导出。这里验证的是“语法与编译链成立”,还没有执行模块,也没有证明页面 CSS、组件依赖和服务端渲染正确。
AsciiDoc 章节放在 docs/partials/install.adoc:
== Install
Run `npm ci` from the repository root.主文件 docs/handbook.adoc:
= Service Handbook
:toc:
:sectanchors:
:source-highlighter: rouge
include::partials/install.adoc[]
== Verify
[source,bash]
----
npm test
----执行完整正向链路:
npm run docs:lint
npm run docs:mdx
bundle exec asciidoctor --safe-mode=safe --failure-level=WARN \
-B docs -D build/html docs/handbook.adoc
bundle exec asciidoctor-pdf --safe-mode=safe --failure-level=WARN \
-B docs -D build/pdf docs/handbook.adoc四个命令都应以 0 退出,分别产生 Markdown 零违规、MDX 编译模块、build/html/handbook.html 和 build/pdf/handbook.pdf。打开最终 HTML 和 PDF,确认目录能跳转到 Install 与 Verify,代码块未丢失,字体可显示中文,图片路径没有逃出构建目录。
反向实验一:同一段 Markdown 为什么会渲染不同
把下面内容保存为 docs/flavor.md:
# Release
| item | state |
| --- | --- |
| api | done |
- [x] build
<details><summary>log</summary>
```text
ok
```
</details>CommonMark 基线不承诺 GitHub 风格表格和任务列表,原始 HTML 是否透传也由处理器配置决定。一个渲染器可能显示完整表格,另一个会把竖线当普通文本,严格安全模式还可能删除或转义 <details>。
故障证据不是“页面看起来怪”,而是同一输入在两个权威命令下产生不同 AST 或 HTML。修复有三种:统一到一个渲染器;把扩展写入格式契约并补快照测试;或改写成 CommonMark 交集。不要靠作者记住“哪个页面不能用表格”。
反向实验二:MDX 不是无害内容
把下面文件只放在隔离实验环境,不要交给线上渲染服务:
export const leaked = process.env.DOC_BUILD_SECRET
# Untrusted document如果应用使用 evaluate 或 run 执行它,导出的 leaked 就能读取构建进程环境变量。MDX 官方对 evaluate 的定义就是“编译并运行”,并明确提示该 API 会执行 JavaScript,详见 MDX evaluate。真正的故障证据是文档模块拿到了 DOC_BUILD_SECRET,而不是页面是否显示了这段值。
安全设计应在信任边界处改变格式:
内部受信作者可以提交 MDX,但按源代码执行 CODEOWNERS、依赖审查、静态扫描和隔离构建。外部投稿只接收不含原始 HTML 的 Markdown 子集,服务端解析后再做 HTML sanitization。需要交互时,由应用把允许的结构化数据映射到组件,不让作者自由写 import、export、表达式或任意 JSX。
必须处理不可信 MDX 时,使用无凭证、只读文件系统、禁网、CPU/内存/超时限制和可销毁进程的隔离环境;单靠组件 allowlist 不能消除 JavaScript 执行风险。
构建进程只能获得读取文档源和写入输出目录的权限。发布令牌、云凭证、生产数据库连接串不应注入文档编译 job。插件和 loader 同样是构建时代码,升级时要像业务依赖一样审查来源、维护状态和依赖树。
反向实验三:挡住越界 include
在 docs/handbook.adoc 中临时加入:
include::../private.txt[]然后运行:
bundle exec asciidoctor --safe-mode=safe --failure-level=WARN \
-B docs -D build/html docs/handbook.adoc预期出现类似 include file has illegal reference to ancestor of jail 的警告,并因 --failure-level=WARN 返回非零退出码。AsciiDoc include 在结构解析之前展开,嵌套 include 还会继续进入预处理器;路径解析和限制规则见 AsciiDoc include directive。
不要用 opts=optional 隐藏必需章节缺失。它适合真正可选的内容;关键安装步骤、法律声明和安全约束缺失时必须让构建失败。
远程 include 风险更高。Asciidoctor 默认不允许 URI include,只有较低安全模式配合 allow-uri-read 才能启用。生产构建应把外部内容固定到仓库或校验过的制品,不在构建时读取会漂移的 URL。确需远程资源时固定版本与摘要,限制域名、网络出口、响应大小和超时,并记录来源许可。
include、插件和原始 HTML 的权限模型
文档处理器通常经历“读取源文件 -> 预处理 -> 解析 AST -> 插件变换 -> 渲染 -> 写制品”。风险取决于扩展在哪一层运行:
Markdown 原始 HTML 会绕过一部分结构化约束,最终输出如果面向不可信读者输入,应使用成熟 sanitizer 并限制 URL scheme。MDX 的 remark、rehype、recma 插件和 JSX 组件都在构建进程权限下运行。AsciiDoctor 的 Ruby 扩展、模板和图表扩展也能执行代码或启动外部程序。
因此扩展准入至少记录包名、来源、锁定版本、维护 owner、需要的文件/网络权限、输入输出、失败语义和退出方案。配置文件应使用 JSON/YAML 等数据格式;允许加载 .cjs、.mjs 或 Ruby 文件时,要明确它们是代码而不是普通配置。
Pandoc 的 --sandbox 可以限制 reader/writer 读取命令行未声明的文件,但不限制 filters,也不覆盖 PDF 生成链的全部 I/O。处理不可信输入时还要禁用未经审查的 filter、自定义 writer 和原始 HTML,限制进程资源,并净化最终 HTML;边界见 Pandoc security note。
构建与导出要保留结构证据
Markdown 导出 HTML 时应显式声明输入方言。下面用 Pandoc 把 CommonMark 输入转换成独立 HTML:
pandoc --from=commonmark --to=html5 --standalone \
--toc --output=build/guide.html docs/guide.md--from 防止 Pandoc 自动采用自己的扩展 Markdown;--standalone 生成包含文档外壳的 HTML;--toc 根据标题结构生成目录。Pandoc 先把输入解析为内部 AST,再由 writer 输出目标格式,因此能保留结构元素,却不保证保留源格式的布局细节;复杂表格、平台容器和自定义组件可能发生有损转换,模型说明见 Pandoc User's Guide。
生成 PDF 有两条常见链:
# Markdown 经 Pandoc 和选定 PDF engine
pandoc --from=commonmark --pdf-engine=xelatex \
--output=build/guide.pdf docs/guide.md
# AsciiDoc 经专用 PDF converter
bundle exec asciidoctor-pdf --safe-mode=safe --failure-level=WARN \
-B docs -D build/pdf docs/handbook.adocPandoc PDF 还依赖 TeX、Typst、wkhtmltopdf 或其他 engine;字体、模板、语法高亮和系统库都必须锁定。AsciiDoctor PDF 直接使用 Prawn 系列库生成 PDF,主题文件控制页面样式。两个 PDF 不能只比较“文件存在”,还要检查页数、目录、标题、交叉引用、代码换行、中文字体、图片和可复制文本。
MDX 不应通过 Pandoc 假装无损导出。更稳定的做法是让站点构建器渲染 MDX 页面,再为打印路由提供专用 CSS 或静态导出;需要归档时,另行定义可移植的内容模型。交互组件在 PDF 中应降级为表格、静态图或链接,并用快照验证降级结果。
生成物通常不提交 Git,而是进入 CI artifact 或制品仓库。若法规或离线交付要求提交 PDF,应把源提交、构建镜像 digest、依赖锁、主题版本和制品摘要关联起来,否则二进制文件无法证明由哪份源生成。
Git diff 要服务评审,而不是制造格式噪声
纯文本格式的优势只有在换行、格式化和生成物策略稳定时才成立。仓库统一 UTF-8、LF、文件末尾换行,并用 .gitattributes 固定文本归一化:
*.md text eol=lf diff=markdown
*.mdx text eol=lf diff=markdown
*.adoc text eol=lf diff=asciidoc
*.pdf binary为 Markdown 和 AsciiDoc 配置标题函数后,git diff 能在 hunk 头显示附近章节。对大段文字修改,使用词级 diff:
git diff --word-diff=color -- docs/
git diff --check -- docs/--word-diff 仍基于行级 hunk,再计算词变化;它适合人工阅读,不应被脚本解析。脚本需要稳定输出时使用 --word-diff=porcelain,具体模式见 git-diff documentation。
避免自动格式化器在迁移提交中重排所有列表和换行。一次迁移先做机械转换并保留映射,下一次提交再修语义;评审者才能区分“格式变化”和“内容变化”。图片、PDF 和快照很大时使用制品仓库或 Git LFS,但必须同时保留可评审的源文件。
链接、标题和代码块组成最小门禁
markdownlint-cli2 可以让编辑器和 CI 共用 Markdown 配置。项目根目录创建 .markdownlint-cli2.jsonc:
{
"globs": ["docs/**/*.md"],
"ignores": ["build/**", "vendor/**"],
"config": {
"MD001": true,
"MD024": {"siblings_only": true},
"MD025": true,
"MD040": true,
"MD041": true
}
}这些规则分别约束标题递增、同级标题重复、单一 H1、围栏代码语言和首行 H1。若站点用 frontmatter 生成页面 H1,MD041 需要按项目模型关闭或定制;不能为了让检查变绿而再写一个重复 H1。CLI 安装、配置优先级和跨平台 glob 规则见 markdownlint-cli2。
不要直接把同一组规则扩到 .mdx。markdownlint 会把 ESM 导出视为 H1 之前的正文,把 JSX 组件视为原始 HTML,于是合法 MDX 会触发 MD041 和 MD033。MDX 应通过 @mdx-js/mdx 解析后的 AST 检查标题递增、唯一页面标题、代码块语言、禁用 import/export 和组件 allowlist,再执行真实打包器构建;这比在 Markdown 规则里批量关闭冲突项更能保留安全语义。
下面的 scripts/check-mdx.mjs 适合“作者只能调用平台组件”的内容模式。它递归读取 .mdx,在编译前拒绝 ESM、未知组件、跳级标题和无语言代码块:
import {readdir, readFile} from 'node:fs/promises'
import {join} from 'node:path'
import {compile} from '@mdx-js/mdx'
const allowedComponents = new Set(['Callout', 'ApiPlayground'])
async function collect(dir) {
const entries = await readdir(dir, {withFileTypes: true})
const nested = await Promise.all(entries.map((entry) => {
const path = join(dir, entry.name)
return entry.isDirectory() ? collect(path) : path.endsWith('.mdx') ? [path] : []
}))
return nested.flat()
}
function remarkDocPolicy() {
return (tree, file) => {
const errors = []
let h1Count = 0
let previousHeading = 0
function walk(node) {
if (node.type === 'heading') {
if (node.depth === 1) h1Count += 1
if (previousHeading && node.depth > previousHeading + 1) {
errors.push(`heading jumps from H${previousHeading} to H${node.depth}`)
}
previousHeading = node.depth
}
if (node.type === 'code' && !node.lang) errors.push('fenced code block has no language')
if (node.type === 'mdxjsEsm') errors.push('import/export is disabled in content MDX')
if ((node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
node.name && /^[A-Z]/.test(node.name) && !allowedComponents.has(node.name)) {
errors.push(`component is not allowed: ${node.name}`)
}
for (const child of node.children || []) walk(child)
}
walk(tree)
if (h1Count !== 1) errors.push(`expected exactly one H1, found ${h1Count}`)
if (errors.length) throw new Error(`${file.path}:\n- ${errors.join('\n- ')}`)
}
}
for (const path of await collect('docs')) {
await compile({path, value: await readFile(path)}, {remarkPlugins: [remarkDocPolicy]})
console.log(`checked ${path}`)
}需要文档内定义组件或导入模块时,应把该目录切换为受信代码模式,保留 ESM 并走完整代码评审、依赖扫描和隔离构建;不要悄悄删掉上面的 ESM 拒绝逻辑,让两种信任模型混在一起。
链接门禁分两层。每次提交快速验证相对路径、锚点和本地图片;定时任务再访问外部 URL,因为公网限流、维护窗口和地域网络会制造噪声。lychee.toml 可以这样起步:
cache = true
max_retries = 2
timeout = 15
accept = [200, 204, 206, 429]
exclude_path = ["build", "vendor"]lychee --config lychee.toml "docs/**/*.{md,mdx,adoc}"外链检查访问私有仓库时使用只读、短期凭证,并避免在日志打印带签名的 URL。检查公共 GitHub 链接时,无额外权限的 token 可提升限额;组织级规模可以改用 GitHub App,凭证建议见 lychee repository。429 是否暂时接受取决于任务类型:PR 快速检查可记录并重试,周期性全量检查最终必须形成待修复结果。
代码块门禁不应只检查有没有语言标签。对 bash、PowerShell、JSON、YAML、JavaScript 等可执行片段,应抽取到临时目录运行解析器或真实命令;带破坏性的示例只做静态检查,并要求显式占位符和隔离环境。最少记录文件、代码块序号、语言、执行命令、退出码和标准错误。
AsciiDoc 构建以 --failure-level=WARN 把缺失 include 和结构警告升级为失败。MDX 同时执行编译、ESLint/TypeScript、允许组件清单和打包器构建。最终站点还要在无缓存环境运行一次,防止作者本机缓存掩盖缺失依赖和大小写路径问题。
项目接入:一条本机和 CI 共用的命令
把门禁收敛到 docs:check,本机、pre-commit 和 CI 调用同一入口:
{
"scripts": {
"docs:check": "npm run docs:lint && npm run docs:mdx && npm run docs:links:local",
"docs:links:local": "lychee --offline --include-fragments \"docs/**/*.{md,mdx,adoc}\""
}
}CI 再安装 Ruby 依赖并运行 AsciiDoc HTML/PDF、站点构建和外链检查。权限从只读开始:
permissions:
contents: read
steps:
- uses: actions/checkout@<FULL_COMMIT_SHA>
- uses: actions/setup-node@<FULL_COMMIT_SHA>
with:
node-version: lts/*
cache: npm
- run: npm ci
- run: npm run docs:check
- run: bundle config set path vendor/bundle
- run: bundle install
- run: npm run docs:adoc
- run: npm run docs:pdf真实工作流应把第三方 action 固定到完整提交 SHA,并由依赖更新机器人维护。构建文档不需要 contents: write;发布动作放到独立 job,只接收已检查的 artifact,并通过环境审批获得短期发布凭证。这样恶意 MDX 或插件即使进入编译阶段,也拿不到仓库写权限和生产 secret。
缓存只保存可重建依赖,不缓存包含凭证的工作目录。Node、Ruby、Pandoc/PDF engine、字体和系统包都进入构建清单。一次干净构建的峰值内存、耗时、输出体积和页面数构成容量基线;变更后比较趋势,而不是设一个适用于所有项目的固定秒数。
从失败现象定位到具体层
编辑器正常,CI 报未知语法
先比较文件扩展名、解析器、插件列表和 lockfile。作者编辑器可能启用了 GFM 或站点私有容器,而 CI 使用 CommonMark。修复后在 CI 使用相同输入生成 AST/HTML 快照,再重开编辑器验证;不要安装更多插件来碰运气。
MDX 报 Unexpected token 或组件未定义
Unexpected token 常来自 ESM-only 包被 CommonJS 加载、MDX 版本不一致、JSX runtime 配置错误或把 .md 当 .mdx。先输出 Node、@mdx-js/mdx、打包器和 runtime 版本,再单独执行核心编译器。组件未定义时,开发模式编译会给出源位置;检查组件是否 import、由 provider 注入,或者是否被 allowlist 拒绝。MDX 2 起采用自动 JSX runtime,classic runtime 相关选项已经弃用并计划移除,迁移提示见 Troubleshooting MDX。
AsciiDoc 生成了 HTML,却返回成功和警告
Asciidoctor 默认让 WARNING 和 ERROR 继续转换,所以“文件存在”不代表正确。检查 stderr,启用 --failure-level=WARN;需要堆栈时加 --trace。缺失 include 的输出中通常会保留 Unresolved directive,故障级别说明见 Asciidoctor errors and warnings。
本地图片可见,发布后 404
检查路径大小写、相对基准、base URL、静态资源复制规则和导出目录。Windows/macOS 的默认文件系统可能容忍大小写错误,Linux CI 会暴露它。链接检查要在构建后的 HTML 上再跑一次,源文件通过只能证明源路径存在,不能证明发布路由正确。
PDF 中文为空白或代码越界
列出构建容器中的字体,确认主题引用的字体文件存在且许可允许分发。代码块要验证长行换行、连字符、等宽字体和页面裁切。修复后比较页数、文本提取结果和关键页面截图;只看 PDF 打开成功会漏掉字体回退和不可复制文本。
迁移时先保语义,再收敛扩展
Markdown 迁到 MDX 的第一步通常只是把扩展名改为 .mdx 并通过编译,随后才把少量特殊容器替换为组件。不要一次性把所有表格、链接和代码块组件化,否则 Git 历史会失去可读性,内容也被框架绑定。
MDX 退回 Markdown 时,先为每个组件定义静态降级:提示框变引用块,交互图变图片加数据链接,API playground 变 curl 示例。构建脚本在发现没有降级规则的组件时失败,不能静默删除。
Markdown 迁到 AsciiDoc 可用 Pandoc 生成初稿:
pandoc --from=gfm --to=asciidoc \
--wrap=none --output=build/migrated.adoc docs/guide.md接着人工核对标题 ID、内部链接、表格、脚注、图片、提示框和代码块。转换是有损映射,不把命令成功当成语义等价。对大批量迁移,先生成清单:源路径、目标路径、源标题、目标锚点、资源数、外链数、未映射节点和 owner;所有旧 URL 都有重定向后再删除旧源。
AsciiDoc 迁回 Markdown 时,条件内容和 include 必须先在明确属性集下展开。每个产品变体单独生成目标,避免把互斥条件混进同一 Markdown。源删除前保留一轮双构建对比,但双份内容只能是短期迁移状态,不能成为长期编辑入口。
回滚依赖可逆边界:保留旧构建命令、旧主题/插件锁和源到目标映射;新格式没有通过链接、标题、代码、视觉和发布验证时,不切换权威入口。发生问题时回退发布路由和构建镜像,不用手工修补生成物。
容量、成本与敏感数据会决定长期形态
文档系统的容量不只看源文件大小。MDX 构建成本受页面数、组件依赖、语法高亮和打包 chunk 影响;AsciiDoc 受 include 图、PDF 字体、图片解码和长表格影响;外链检查受 URL 数量、限流和网络延迟影响。团队应观察干净构建耗时、峰值内存、输出字节、页面数、最大页面、外链总数与失败年龄,并按趋势扩容或拆分任务。
成本包括 CI 分钟、artifact/搜索索引存储、PDF 引擎镜像、商业字体、编辑器插件、托管平台席位和维护 owner 时间。三种格式及其核心开源处理器本身不等于完整的免费平台;托管预览、私有搜索、SSO、审计和协作权限可能属于供应商团队或企业方案,采购前必须到对应官方定价和权限页面确认,不能从格式能力推导商业权益。
文档源、include 文件、代码样例、图片 EXIF、构建日志和搜索索引都可能携带敏感数据。禁止把真实 token、Cookie、客户标识、内部域名、私有仓库 URL 和未脱敏截图放进示例。秘密扫描应覆盖 .md、.mdx、.adoc、生成 HTML、source map 和 CI artifact;命中后按真实泄漏处理,轮换凭证并清理 Git 历史,不能只删当前行。
搜索和分析服务只接收允许公开或内部检索的最终文本。MDX 组件 props 和隐藏标签、AsciiDoc 条件分支、HTML 注释也可能进入索引。权限模型至少区分公开、组织、项目和受限文档,离职回收、分享链接、导出文件和搜索缓存使用相同的数据分级。
让格式契约成为可维护资产
每个文档域需要一个明确 owner,负责解析器、插件、主题、组件 allowlist、链接豁免、构建镜像、发布权限和迁移计划。仓库记录以下可执行事实:
哪些后缀由哪个解析器和版本线处理,允许哪些扩展。本机预览、CI 构建和发布使用哪一条权威命令。include 根目录、远程资源、原始 HTML、MDX import/export 和扩展加载采用什么策略。
标题 ID、链接、代码块、图片、PDF 和可访问性由哪些检查签发。生成物保存多久,谁能下载,如何关联源提交和依赖锁。插件升级、格式迁移、发布失败和平台退出如何回滚。
依赖更新先在隔离分支运行干净构建和关键页面视觉对比。解析器大版本升级重点检查标题 slug、脚注、原始 HTML、表格、代码高亮、组件绑定和 source map;PDF 后端升级重点检查字体、分页、目录和主题变量。允许短期锁在旧版本,但必须有 owner、风险记录和升级窗口,不能无限期依赖已结束维护的扩展。
团队可以把下面这组不变量作为发布证据:所有源文件都有唯一解析器;本机与 CI 使用同一锁文件;必需 include 不会被静默跳过;不可信内容不会进入 MDX 执行链;内部链接和锚点全量通过;外链失败有年龄与 owner;代码块可解析或有明确豁免;生成物可追溯到源提交;构建 job 没有多余写权限和生产凭证;连续多轮构建的耗时、内存和产物体积没有无解释地单调增长。
做到这些,格式就不再是个人编辑器偏好,而是一条能安装、预览、评审、构建、排障、迁移和退出的文档生产链。
