从一段没有画出来的流程图开始:给 NAS 上的 Go Markdown 服务补上 Mermaid

本文最后更新于 2026年7月13日 晚上

先说结论:这次流程图显示成源码,不是 Mermaid 写错了,也不是 Go “不支持画图”。

原来的小服务已经把 Markdown 排成了网页,却没有带上真正负责画图的 Mermaid。我们做的事情,就是把这位“绘图员”装进程序,只在需要时请它工作,再把新版本装回 NAS。

这篇文章特意写给刚接触编程的人。你不需要先学完 Go、Docker 或前端,跟着“谁负责哪一步”往下看,就能理解这次改造为什么有效。

0. 这次完成了什么

我原本在 NAS 上运行一个很小的 Go Markdown 服务。普通标题、列表、表格都能显示,但 Mermaid 流程图会原样出现在灰色代码框里。

最终完成了这些事:

  • 找到真正缺失的环节,而不是继续修改已经正确的流程图;
  • 从正在使用的上游项目 fork 出自己的版本;
  • 把 Mermaid 11.16.0 放进 Go 程序和 Docker 镜像,不依赖公网 CDN;
  • 普通文章不加载绘图工具,有图的文章才加载;
  • 图写错时保留源代码,不让整页内容消失;
  • 通过 Go 测试、真实浏览器和 NAS 上的 Docker 三层验证;
  • 把成果提交并推送到自己的 GitHub 仓库。

代码在这里:

1. 先别怀疑自己:图的写法没有问题

当页面显示下面这些文字,而不是两个方框和一条箭头时,很容易以为是语法写错了:

1
2
flowchart TD
A[开始] --> B[完成]

但“页面能把这段文字完整显示出来”本身就是一条线索:文件读到了,Markdown 也处理了它,只是最后没人把文字画成图。

这很像把一份建筑图纸送到了打印店:

  • 打印店已经认出这是一张名为 Mermaid 的图纸;
  • 它把图纸整齐地装进了文件袋;
  • 但现场没有施工人员,所以你最后看到的仍然是图纸。

原来的处理链路其实是:

flowchart TD
  A["Markdown 原稿"] --> B["gomarkdown 排版"]
  B --> C["HTML 中的代码块"]
  C --> D["浏览器显示 Mermaid 源码"]

最后一步并没有失败。它只是按照普通代码块的方式,忠实地把源码显示了出来。

2. 认识五个角色,就能看懂整个项目

名字很多,但每个角色只负责一件事。

角色 它在做什么 生活中的比喻
Markdown 用纯文字记录标题、表格、代码和图表 原稿
Go 服务 读取文件,组织网页,再送给浏览器 图书管理员
gomarkdown 把 Markdown 的结构转换成 HTML 排版员
Mermaid 把图表文字转换成 SVG 图形 绘图员
浏览器 执行 Mermaid,并把最终网页显示出来 展览厅

Docker 和 NAS 则是外面的两层:

  • Docker 镜像像一个封好的搬家箱,程序和它需要的文件都装在里面;
  • Docker 容器是这个箱子真正打开并开始工作的状态;
  • NAS是长期放置并运行它的那台电脑。

理解到这里已经足够继续排查了。遇到陌生技术时,不必先记住全部细节,先问一句:它在这条链路里负责什么?

3. 我们怎样证明缺的是 Mermaid

排错最怕同时怀疑所有东西。更可靠的方式,是一层一层找证据。

3.1 先看最小示例

我们用最短的 A --> B 流程图测试。它在 Mermaid 10 和 11 中都能成立,因此复杂语法不是主要嫌疑。

3.2 再看 Go 服务输出的网页

gomarkdown 已经把图表围栏转换成了这样的 HTML:

1
<pre><code class="language-mermaid">...</code></pre>

language-mermaid 就像贴在文件袋上的标签:“这里面是一张 Mermaid 图”。这说明解析器没有把语言名称弄丢。

3.3 最后找真正的绘图工具

测试页面里能找到 5 个 language-mermaid 代码块,却找不到 Mermaid 的 JavaScript 文件,也没有任何初始化代码。

所以证据链很清楚:

  1. 原始 Markdown 已经读到;
  2. gomarkdown 已经认出 Mermaid 代码块;
  3. HTML 已经带着正确标签到达浏览器;
  4. 浏览器里没有 Mermaid,自然只能显示源码。

这不是 gomarkdown 的解析错误,而是服务原本没有实现“浏览器绘图”这一步。

4. 为什么原项目没有带 Mermaid

这里需要把“事实”和“推测”分开。

项目没有留下“我们故意拒绝 Mermaid”的说明。根据原 README 和工程结构,更合理的判断是:它的目标边界原本就停在 Markdown 转 HTML,并且很重视轻量。

原项目强调:

  • 一个很小、低内存占用的 Go Web 服务;
  • 使用 scratch 作为 Docker 运行环境,里面连完整操作系统都没有;
  • 尽量少的文件和依赖;
  • 标题、表格、代码块等常见 Markdown 能直接阅读。

Mermaid 则会带来新的成本:

  • 压缩后的 JavaScript 文件本身也有数 MB;
  • 程序需要决定何时加载它;
  • 浏览器脚本会增加安全、缓存和兼容性工作;
  • Mermaid 升级后还要重新测试;
  • 错误图表需要设计降级方式。

因此,不加入 Mermaid 很可能是项目范围和体积之间的取舍,并不代表作者做错了。我们的使用场景更重视流程图,于是在自己的 fork 中选择了另一种平衡。

5. 这些相似的仓库,到底是什么关系

刚看到 gomarkdowngo-markdown-serverlanguage-mermaid 时,我也很容易把它们看成同一个家族。实际关系如下:

名称 真实身份 这次是否要改
nickgrealy/go-markdown-server 可以追溯到的早期 Go 服务项目 不直接改
mountain-pass/go-markdown-server NAS 当时使用的服务源码基础 从这里 fork
gomarkdown/markdown Go 语言的 Markdown 解析库 继续使用,不改
mermaid-js/mermaid 真正把文字画成图的前端库 作为依赖放入程序
gou7ma7/go-markdown-server-with-mermaid- 我自己的改造版 在这里开发

另一个容易混淆的地方是名字:

  • GitHub 组织名是 mountain-pass,中间有连字符;
  • Docker Hub 镜像名是 mountainpass/go-markdown-server,中间没有连字符;
  • class="language-mermaid" 只是 HTML 上的一张语言标签,不表示这个 Go 项目是从 Mermaid fork 出来的。

为什么从 mountain-pass fork

因为 NAS 当时运行的就是它发布的镜像。从相同源码起步,改动最少,旧的 Markdown 路由、样式和 Docker 用法也最容易保持一致。

Fork 可以理解成“复印一份保留出处的公共笔记”。你在自己的副本上做实验,不会直接改坏原作者的项目,以后也仍然能看出它从哪里来。

为什么不直接修改 gomarkdown

gomarkdown 已经完成了它该做的事情:把代码块和 mermaid 语言标签交给 HTML。

如果去修改它,就像要求排版员兼任绘图员。即使解析器输出了不同标签,浏览器里仍然需要真正的 Mermaid 引擎。把功能加在 Go 服务这一层,职责更清楚,影响范围也更小。

6. 我们实际怎样补上绘图能力

改造后的链路是:

flowchart TD
  A["Markdown 文件"] --> B["Go 服务读取"]
  B --> C["gomarkdown 生成 HTML"]
  C --> D{"页面含 Mermaid 吗"}
  D -->|没有| E["直接显示网页"]
  D -->|有| F["加载程序内置的 Mermaid"]
  F --> G["浏览器画成 SVG"]
  F -->|语法错误| H["保留原始代码"]

这里有六个关键选择。

6.1 把绘图工具装进程序

Mermaid 11.16.0 的 JavaScript、初始化脚本和样式文件都放进项目,再用 Go 的 go:embed 打包进可执行文件。

go:embed 可以理解成“封箱前把工具包塞进去”。部署时不需要再去 NAS 上单独找这些文件,也不会因为局域网访问不了公共 CDN 而失效。

6.2 有图时才加载

服务生成 HTML 后,会检查其中有没有 class="language-mermaid"。逻辑可以用接近自然语言的伪代码表示:

1
2
3
4
如果页面里有 Mermaid 代码块:
加载 Mermaid 的 CSS 和 JavaScript
否则:
按原来的方式返回页面

普通文章因此不用下载 3.5 MB 左右的绘图库。轻量项目原本的优点被尽量保留下来。

6.3 先画成功,再替换源码

浏览器先在内存里尝试绘图。只有生成 SVG 成功后,页面上的原始代码框才会被替换。

如果语法写错,代码仍然留在页面上,并出现错误提示。这样至少还能复制源码继续修改,不会只剩一块空白。

这像让绘图员先在草稿纸上完成作品,确认没问题后再换下原稿。

6.4 不依赖公网 CDN

公共 CDN 用起来方便,但 NAS 可能没有稳定外网,CDN 也可能被网络策略拦截。自带 Mermaid 文件后,浏览器只向同一台 NAS 请求资源。

代价是镜像会变大,而且以后升级 Mermaid 时要由我们更新文件。这里选择的是“可离线、可预测”,而不是最小下载体积。

6.5 给浏览器脚本设护栏

Mermaid 使用 securityLevel: "strict",脚本和样式都从服务自身加载,不开放内联脚本。原项目的 Content Security Policy 仍然有效。

这不等于整个 Markdown 服务可以放心接收陌生人上传的任意内容。原渲染器允许原始 HTML,因此目前仍应只展示可信的 Markdown;如果将来开放上传,还需要额外的 HTML 清理。

6.6 让图适应屏幕

生成的 SVG 最大宽度跟随正文区域,小屏幕上不会硬撑破页面。电脑、平板和电纸书都能按各自宽度阅读。

7. 验证不是“感觉能用”,而是逐层确认

我把验证分成三层。每一层回答一个不同的问题。

7.1 Go 层:功能有没有接对

运行:

1
go test ./...

自动测试确认:

  • 普通页面不会加载 Mermaid;
  • 有图页面会加载三个内置资源;
  • 资源的类型和缓存设置正确;
  • 不存在的资源返回 404;
  • 默认安全策略允许自带脚本,但没有放开不必要的内联脚本。

7.2 浏览器层:真的画出来了吗

服务器返回正确 HTML,不等于浏览器一定能画图,所以又准备了真实浏览器测试页。

结果是:

  • 5 个有效图表全部变成 SVG;
  • 页面中剩余的 Mermaid 源码框为 0;
  • 浏览器控制台没有报错;
  • 故意写错的图不会生成错误 SVG,原始代码仍然保留。

这里很重要的一课是:测试要覆盖真正工作的那一层。 Go 测试能检查网页准备工作,最终绘图发生在浏览器,就还要打开浏览器看。

7.3 Docker 与 NAS 层:换到真实环境还能工作吗

最后构建新的 Docker 镜像,在 NAS 上保留原来的内容目录和端口,只替换运行程序。

生产页面返回 HTTP 200,原测试文档中的 5 张图都成功显示,浏览器控制台仍然干净。旧镜像和原 Compose 文件备份都保留着,出现问题可以切回。

部署像换灯泡:先把旧灯泡放在手边,再装新灯泡并通电检查,而不是先把旧灯泡扔掉。

一个与 Mermaid 无关的小插曲

测试 Compose 曾指定容器使用 1001:1001 身份运行,但 NAS 上原有 Markdown 目录的访问规则不允许这个身份读取,于是出现 permission denied

这不是绘图失败,而是“箱子里的人没有书架钥匙”。最终沿用这台 NAS 原有服务的运行身份,并继续把内容目录只读挂载。遇到类似错误时,不要为了省事直接给整个目录开放所有权限,应先确认究竟是哪一个身份需要读哪一个目录。

8. Git 做的不是魔法,而是给过程留存档

这次最后执行了两个动作:

  • commit:给当前成果拍一张带说明的快照;
  • push:把这张快照上传到自己的远端仓库。

提交信息是:

1
feat: add self-hosted Mermaid rendering

提交编号 50b914f 就像这张快照的取件码。以后无论代码继续怎样变化,都能准确回到这一次完成并验证过的状态。

Fork、commit 和 push 可以这样记:

动作 类比
Fork 复印一本公共笔记,并保留原作者信息
修改 在自己的副本上做实验
Commit 给一个阶段拍照并写说明
Push 把照片备份到远端书架

9. 如果你刚开始学编程,真正值得带走什么

这次涉及 Go、HTML、JavaScript、Docker、NAS 和 Git,看起来像要同时学六门课。实际上,解决问题时我们没有从头学完任何一门,而是反复做下面五件事:

  1. 把现象说准确:不是“Markdown 坏了”,而是“Mermaid 源码出现了,图没有出现”。
  2. 给每个角色划职责:解析器负责排版,Mermaid 负责绘图,浏览器负责执行。
  3. 找能被检查的证据:HTML 有语言标签,但页面没有 Mermaid 脚本。
  4. 在正确的一层做小改动:改服务的页面输出,不重写 Markdown 解析库。
  5. 给每一步留退路:错误时保留源码,部署时保留旧镜像,代码用 Git 留快照。

这套方法比记住某一条命令更重要。以后遇到“文件能读却不能预览”“接口有数据却页面没显示”“容器能启动却访问不到目录”,都可以先画出链路,再寻找缺失的那一棒。

看不懂整个项目并不妨碍你解决其中一个具体问题。

能把大问题缩成一句可验证的话,再找到负责那一步的代码,你已经在使用成熟开发者最常用的排错方法了。

10. 一张给下次的排错清单

当 Mermaid 再次没有显示时,可以按这个顺序检查:

  1. Markdown 文件能否正常打开?
  2. 代码块有没有标记为 mermaid
  3. 返回的 HTML 中有没有 language-mermaid
  4. Mermaid JavaScript 是否成功加载?
  5. 浏览器控制台有没有语法或安全策略错误?
  6. 最小的 A --> B 能否显示?
  7. 本地能显示、NAS 不能显示时,资源路径和容器权限是否一致?

一次只回答一个问题。每得到一个确定答案,未知范围就会小一圈。

参考


从一段没有画出来的流程图开始:给 NAS 上的 Go Markdown 服务补上 Mermaid
https://gou7ma7.github.io/2026/07/13/devops/@2026_add_mermaid_to_go_markdown_server/
作者
Roy Lee
发布于
2026年7月13日
许可协议