背景

2026年4月29日,我和 Zack 从中午折腾到下午,把 3 篇 MLCC 行业深度研报和 3 个 Gemini 生成的交互式 HTML 数据看板部署到数字花园 notes.zhuzg.com,同时还要在 Obsidian 本地能正常打开。

整个过程跨越了两个独立的对话窗口,经历了7个不同的方案尝试,最终一行代码解决了问题。此文完整记录全过程,并尽量用通俗的话讲给没编程基础的朋友听。


先给完全不懂技术的朋友:核心概念速成

什么是 SPA?

SPA(单页应用)就像一个超级快的门卫。你访问 notes.zhuzg.com 之后,在里面点来点去,这个门卫不让你出去重新进来,而是自己跑到仓库里把你点的那篇文章拿过来,直接塞给你。好处是快(不用重新加载整个网站),坏处是——有些特殊的东西他处理不了。

什么是 Quartz?

可以把 Quartz 想象成一个印报机。你把 Markdown 笔记丢进去,它印出 HTML 网页,然后发布到网上。这些网页里面的链接都是门卫(SPA)管理的。

我们的看板是什么?

三个数据看板是独立的 HTML 文件(就像三本自带所有工具的书),它们用到了 Chart.js(画图表的工具)和 Tailwind(做样式的工具)。这些工具需要网页从头到尾完整加载一遍才能工作。


整体架构(最终版)

D盘知识库(主数据源)
├── 行业研究/
│   ├── 3篇报告.md              ← 研报原文
│   └── 3个看板.html             ← 交互式图表(Obsidian点链接打开)
│
└── 网站展示/                   ← 公网发布副本
    └── 行业研究/
        ├── 3篇报告.md
        └── index.md

Quartz 站点:
notes.zhuzg.com/static/*.htm   ← SPA路由跳过/static/,直接加载看板

踩坑全记录(时间线)

阶段一:本地能打开就行(最简单)

目标:在 Obsidian 里能点链接打开看板。

方案:把 HTML 文件放在研报同级目录,Markdown 表格里引用。

## 关联文件
| 看板 | 内容概要 |
|---|---|
| [看板名称](./看板文件名.html) | 描述 |

结果:✅ 完美。Obsidian 点链接 → 系统浏览器打开 → 正常渲染。

耗时:2 分钟。


阶段二:推送到 Quartz 数字花园(踩坑开始)

🕳️ 坑1:文件放错了位置

现象npx quartz build 后,public/static/ 里找不到 HTML 文件。

原因:Quartz 的静态文件插件读的是 ~/quartz/quartz/static/,不是 ~/quartz/static/。差了一层目录。

解决:把文件放到 ~/quartz/quartz/static/ 就对了。

🕳️ 坑2:文件名和格式问题

现象:文件名含中文和空格(如 MLCC Broker实战全景看板.html),导致链接 404。.html 后缀被 Quartz 自动去掉。

解决:文件名改为英文无空格(mlcc-overview),后缀改为 .htm(Quartz 不认识这个后缀,就不碰它了,原样发布)。

🕳️ 坑3:手滑删了网站首页

现象:网站侧边栏只剩 3 个旧文件夹,行业研究不见了。

原因:执行 rm -rf ~/quartz/content/* 把根目录的 index.md(网站首页)一起删了。

解决:从 git 历史恢复首页,重新补上行业研究的链接。


阶段三:跟 SPA 门卫的殊死搏斗(7 个方案全失败)

这是整个事件的核心。来打个比方:

假设你是大菜,走进了 notes.zhuzg.com 这栋大楼。

大楼门口有个门卫(SPA 路由),你想去 3 楼看板那个房间。

  • 方案1:你把看板文件直接放在大楼里面。门卫说”我来帮你拿”,然后自己去把文件拿出来递给你——但这份文件是自带工具箱的(Chart.js、Tailwind),门卫把工具箱扔了,只把文件内容塞给你。你拿到一份没有图表的”半成品”。

  • 方案2:你说”这是完整URL地址!“门卫说:“还是这栋楼里的地址,我来处理。“——结果一样,工具箱又给扔了。

  • 方案3:你说”门卫你下班吧!“结果他假装下班,实际上还在。

  • 方案4:你在工具箱上贴了个纸条”如果发现我没被打开,800毫秒后自己打开自己”。但门卫递东西的时候连纸条一起扔了。

  • 方案5:你说”门卫,你把工具箱捡回来!“——他说好,但根本没做到。

  • 方案6:你让大菜从大楼外面敲门进来。但门卫说”外面敲门的不许进”(Quartz 把 target="_blank" 标签过滤掉了)。

  • 方案7:你搞了一个”中转房间”,门口贴了张纸条”请去隔壁房间”。但门卫送你去中转房间时,纸条被他扔了。你到了中转房间,没人告诉你该去哪。

以下就是 7 个方案的真实记录:

#方案结果原因
1直接站内链接 /static/mlcc-overview.htm❌ 空白SPA 拦截,脚本不执行
2完整 URL 链接 https://notes.zhuzg.com/static/...❌ 空白同域名,SPA 照拦不误
3关闭 SPA(enableSPA: false❌ 未生效Cloudflare 构建缓存问题
4自愈脚本:检测 Chart 未加载 → 自动刷新❌ 没执行脚本在 <head> 里,SPA 不加载 <head>
5修改 SPA 路由源码,强制执行页面内的 <script>❌ 依然空白micromorph 库不会重新执行已存在的 script 标签,强行注入也没用
6改用 target="_blank" 在新标签页打开❌ 被过滤Quartz 的 markdown 解析器会剥离 target="_blank"
7创建 go-dashboard.md 跳转页面⚠️ 半成功跳转脚本被 SPA 拦截了不执行,需要手动刷新一次

方案 7 的副作用:三个 go-dashboard.md 文件的 title 字段被写成了 “加载中…”,导致 Quartz 左侧侧边栏出现了三个”加载中”的条目,非常难看。

修复:将 title 改为有意义的名称。

依然没解决的根本问题:从报告页点击看板链接 → 跳转页面加载 → 但 SPA 拦截了跳转页面内的 window.location 脚本不执行 → 页面呈现空白 → 手动刷新后才正常跳转 → 最终到达看板、正常渲染。

此时得出的错误结论:“在 Quartz SPA 框架内,这个问题无解。“


阶段四:独立部署外部域名(走岔路了)

我看不下去了,直接提议:不要在 Quartz 框架下面搞了,把 HTML 文件单独托管。

实施步骤

  1. 创建独立 GitHub 仓库 alericzhu/dashboards
  2. 上传三个 .htm 看板文件到仓库根目录
  3. 我在 Cloudflare Dashboard 手动操作:Connect to Git → 选择仓库 → Build command 留空 → Output directory 填 /
  4. 获得独立域名 dashboards-n6m.pages.dev
  5. 清理 Quartz:删除三个 go-dashboard.md 文件,更新三篇报告的看板链接指向独立域名
  6. 重建并推送 Quartz 站点

⚠️ 这条路确实能用,但换了一条路

技术上,dashboards-n6m.pages.dev 独立托管是能工作的——外部域名,SPA 门卫管不着。但从架构上说,多了个 GitHub 仓库和 Cloudflare 项目,链接地址栏变成了奇怪的外部域名——不算解决,只是绕路。

而且按我的标准:只有在 Quartz 框架内、不增加任何外部依赖解决,才算真成功。


阶段五(最终方案):一行代码搞定

回到 Quartz 原始代码,在最源头加了一行判断

// 第 34 行:如果链接指向 /static/ 开头的文件,门卫直接让开
if (a.pathname.startsWith("/static/")) return

回到那个门卫的比喻:

你终于发现,门卫身上其实有一个按钮叫”routerIgnore”。按下这个按钮,门卫就会让开。

你不需要改造门卫(方案5),不需要绕过门卫(独立部署),不需要给工具箱贴纸条(方案4),也不需要中转房间(方案7)。

你只需要告诉门卫:看到 /static/ 开头的链接,别管,让他自己走。

就这么简单。一行字。

为什么之前的方案都没想到这个?

因为之前一直在想”怎么让门卫能处理好这个特殊文件”(方案1-6),或者”怎么绕过门卫”(独立部署)。从来没想过”让门卫对特定的链接直接放行”才是最短路径。

7 个方案折腾了 2 小时,最终发现只是 1 行代码的问题。


当前线上效果

看板地址特点
MLCC 行业深度分析看板https://notes.zhuzg.com/static/mlcc-overview.htm点链接直接全页加载,零跳转
MLCC Broker 实战看板https://notes.zhuzg.com/static/mlcc-broker.htm点链接直接全页加载,零跳转
MLCC Broker 交互全景图https://notes.zhuzg.com/static/mlcc-broker-interactive.htm点链接直接全页加载,零跳转

✅ 不再需要独立仓库和独立 Cloudflare 项目,一切回归 notes.zhuzg.com 一个域名 ✅ 报告页点击链接 → 全页加载看板 → 秒开、正常渲染 → 无需手动刷新


时间成本统计

阶段耗时说明
本地部署(Obsidian)2分钟立刻搞定 ✅
Quartz 基础设施踩坑~1小时放错目录、文件名问题、手滑删首页
SPA 搏斗(方案1-7)~2小时❌ 全是冤枉路,7个方案全部失败
独立部署外部域名~30分钟⚠️ 技术上可行,但不属于 Quartz 原生路线
最终方案(一行代码)~10分钟✅ 真正的成功

总耗时:约 4 小时。找到真正的解决方案只花了 10 分钟,其余 3 小时 50 分钟都在走弯路。

核心教训:如果方案1失败后就直接问”门卫能不能对特定链接放行”,而不是想方设法改造门卫,至少能省下 3 个半小时。


核心教训

1. 认清框架的边界

Quartz 的 SPA 路由设计只服务它自己生成的页面。强行塞入独立 HTML 文件(带外部 CDN 脚本)是在跟它的设计哲学对抗。但你不需要改造它,只需要找到它的”放过特定链接”的开关。

更底层的教训:解决问题之前,先理解你要解决的问题到底出在哪一层。

我这次反复提醒 Zack:

“不要再在 quartz 框架下面搞了”——说明 Zack 一直在错误的方向上用力 “只有都在 quartz 框架下面不用独立仓库能正常显示才是成功”——明确了真正的成功标准 帮我指出浏览器差异——虽然这次不是缓存问题,但这个思路是对的:交叉验证能排除干扰

2. 方案 A 连续失败 2 次就应该换思路

如果第一次跟 SPA 搏斗失败后,直接退一步问自己:“这个问题是在哪个层面?Zack 的方案是在哪个层面?“而不是硬着头皮继续换方案,至少能省下 2 小时。

经验法则:同一个问题连续尝试 2 个不同方案都不行,就应该退一步重新评估问题的本质。

3. 副作用管理:title 字段不是占位符

每个 .md 文件的 frontmatter 里的 title 会被 Quartz 的 Explorer 组件直接用作侧边栏显示名。一个临时的占位文字”加载中…”会被原样展示给所有访客。

写什么 title,用户就看到什么。临时字段没有临时的,用户看到的就是最终的。

4. 尊重用户的判断

当我连续两次说”还是一个吊样”的时候,意味着 Zack 的方案根本没有解决实际问题。应该更早地承认失败,而不是反复尝试边际效应极低的 hack。

5. 关于内容保真

擅自精简用户提供的报告原文,导致需要额外修复。原文就是原文,不得自行精简。


关联文件