1. 问题现象

Mooglade 从 v16.2.0 开始支持 Mermaid 渲染,在文章中使用 Markdown Mermaid 代码块时,本地开发环境可以渲染图表,但线上仅显示 Mermaid 源代码,不生成 SVG 图表。

受影响的 Markdown 格式示例:

```mermaid
flowchart LR
  A --> B
```

2. 初始排查

  1. 检查线上文章生成的 HTML。

  2. 确认 Markdown 已正确转换为:

    <pre><code class="language-mermaid">...</code></pre>
    
  3. 确认文章页面已加载带指纹的资源:

    /js/3rd/mermaid.min.zfjo3jmytt.js
    /js/app/post.2xfib7zcr8.mjs
    
  4. 检查浏览器运行状态:

    typeof globalThis.mermaid
    

线上结果为 undefined,而本地预览页结果为 object

  1. 检查渲染结果:

    document.querySelectorAll('.post-content pre.mermaid svg').length
    

线上为 0,本地为 1

3. 前端渲染机制

文章页加载 post.mjs,其调用 post.mermaid.mjs

  1. 查找 .post-content pre code.language-mermaid.lang-mermaid
  2. globalThis.mermaid 获取 Mermaid API。
  3. 将代码块转换为 pre.mermaid
  4. 调用 mermaid.run({ nodes }) 生成 SVG。

由于线上 globalThis.mermaid 不存在,渲染函数会直接返回,不会抛出明显错误。

4. 根因

本地与线上原始 Mermaid JavaScript 文件的 SHA-256 相同,不是版本或源码差异。

差异在 HTTP 内容协商:

  • 本地 Kestrel 返回有效的 gzip 资源。
  • 线上浏览器请求头包含 Accept-Encoding: br, gzip,优先获得 Brotli (br) 资源。
  • 发布目录中的 /path/to/app/wwwroot/js/3rd/mermaid.min.js.br 初始大小为 0 字节。
  • 发布清单 /path/to/app/Mooglade.staticwebassets.endpoints.json 中,Mermaid Brotli 条目的 Content-Length 也为 0,ETag 是空内容哈希。

因此浏览器表面上成功加载了 Mermaid 脚本 URL,实际执行的是空响应,导致 globalThis.mermaid 未定义。

5. 修复动作

  1. 安装 Brotli 命令行工具:

    apt-get update
    apt-get install -y brotli
    
  2. 从原始脚本重新生成 Brotli 副本:

    brotli -f -q 11 /path/to/app/wwwroot/js/3rd/mermaid.min.js
    
  3. 修复静态资源清单中所有指向 js/3rd/mermaid.min.js.br 的条目。

    需要同步更新以下字段:

    • Content-Length:实际 .br 文件长度
    • ETag.br 文件的 SHA-256 Base64 值,带双引号
    • EndpointProperties 中的 integritysha256- 加相同的 SHA-256 Base64 值

    本次共修复 4 个路由:

    js/3rd/mermaid.min.js
    js/3rd/mermaid.min.js.br
    js/3rd/mermaid.min.zfjo3jmytt.js
    js/3rd/mermaid.min.zfjo3jmytt.js.br
    
  4. 修改清单前已创建备份:

    /path/to/app/Moonglade.Web.staticwebassets.endpoints.json.bak-YYYYMMDDHHMMSS
    
  5. 重启应用加载新的静态资源清单:

6. 修复验证

验证 Brotli 响应:

curl --compressed -sS -D - -o /dev/null \
  -H 'Accept-Encoding: br, gzip' \
   https://example.com/js/3rd/mermaid.min.<hash>.js

预期响应:

Content-Encoding: br
Content-Length: 739278

浏览器端验证:

({
  mermaidPresent: typeof globalThis.mermaid !== 'undefined',
  renderedMermaidBlocks: document.querySelectorAll('.post-content pre.mermaid svg').length
})

本次修复后线上结果:

{
  mermaidPresent: true,
  renderedMermaidBlocks: 1
}

7. 用户侧缓存处理

Mermaid 带指纹资源使用:

Cache-Control: max-age=31536000, immutable

故障期间已缓存空 Brotli 响应的浏览器可能不会在普通刷新时重新请求资源。用户需使用强制刷新:

  • Windows/Linux:Ctrl + Shift + R
  • macOS:Command + Shift + R

8. 后续部署注意事项

  1. 下次发布时,必须将原始静态文件、.gz/.br 预压缩文件和应用的 staticwebassets.endpoints.json 清单作为同一批发布产物整体传输。

  2. 不要只覆盖 DLL 或原始 JavaScript 文件,否则压缩文件与静态资源清单可能失配。

  3. 发布完成后抽查关键资源的压缩响应,尤其是浏览器会优先请求 Brotli 的环境:

    curl --compressed -sS -D - -o /dev/null \
      -H 'Accept-Encoding: br, gzip' \
    https://example.com/js/3rd/mermaid.min.<hash>.js
    
  4. 若再次出现同类问题,应优先检查:

    stat -c '%s %n' /path/to/app/wwwroot/js/3rd/mermaid.min.js*
    

    以及静态资源清单中的 Content-LengthETagintegrity 是否与实际压缩文件一致。

  5. 本次服务器端清单修复是发布产物损坏后的补救措施。长期解决方案是修复或校验部署流程,确保发布前生成并验证所有预压缩静态资源。