背景
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 文件单独托管。
实施步骤
- 创建独立 GitHub 仓库
alericzhu/dashboards - 上传三个 .htm 看板文件到仓库根目录
- 我在 Cloudflare Dashboard 手动操作:Connect to Git → 选择仓库 → Build command 留空 → Output directory 填
/ - 获得独立域名
dashboards-n6m.pages.dev - 清理 Quartz:删除三个 go-dashboard.md 文件,更新三篇报告的看板链接指向独立域名
- 重建并推送 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. 关于内容保真
擅自精简用户提供的报告原文,导致需要额外修复。原文就是原文,不得自行精简。
关联文件
| 文件 | 说明 |
|---|---|
| MLCC行业深度研究报告(综合篇) | 第一篇研报 |
| MLCC行业深度研究报告(定价机制篇) | 第二篇研报 |
| MLCC供应链深度剖析(Broker篇) | 第三篇研报 |