Quartz:把 Obsidian 知识库变成网站
一个笔记库最憋屈的状态不是写得少,是只有本地那台电脑见过它。思维模型格栅那个 vault 里躺着 151 篇 markdown:121 张模型卡、16 个学科、9 个使用场景,再加 4 篇索引与模板规范和一篇首页。写文章前翻它拉论证骨架,写完把新案例回填到卡片里,这套循环跑了大半年,一直只在一台 Mac 的 iCloud 目录里生效。
Quartz 就是把它变成网站的工具。仓库 13,295 stars、MIT 许可、TypeScript 写的,作者 Jacky Zhao,README 里那句说明很直白:把 digital garden 和笔记免费发布成网站。我们本地锁的是 v5.0.0,仓库最近一次提交在 2026 年 9 月 27 日。线上站点 www.golfr20.cn 里,155 条条目可以检索。
那个库的组织方式是四层:01-三维坐标系.md 定场景 × 学科 × 模型的坐标,02-学科索引.md 和 03-场景索引.md 各维护一张入口表,04-模板与规范.md 管卡片格式,剩下 146 篇是内容。这套结构在 Obsidian 里靠文件名前缀加 wikilink 维持,搬到网站上不用改一个字,Quartz 只要求它们是 markdown。
双链是构建期产物,不是运行时功能
Quartz 把「笔记之间的关系」当成编译对象,这一点比它渲染出来好不好看重要得多。wikilinks、callouts、块引用都在构建时解析成真实链接,反链、关系图谱、全文搜索索引全部由同一份链接数据生成:
- 每篇笔记右侧的反链区块,列出引用过它的笔记
- 右侧的关系图谱,节点与边就是构建期算出来的链接
- 搜索索引落在
static/contentIndex.json,浏览器本地检索,没有搜索服务 - 鼠标停在链接上弹出目标笔记的首段(
enablePopovers),不用点进去 - 站内跳转走 SPA(
enableSPA),不整页刷新
我们那份构建产物:156 个 HTML 文件、5.9 MB,搜索索引 338 KB 装着 155 条条目。换成时间看得更清楚——解析 151 篇 markdown 用了 471 毫秒,写出 210 个文件用了 205 毫秒,单线程(-c 1)跑完 705 毫秒。整个站没有数据库、没有后端、没有运行时查询,打开就是一串静态文件。
这是它和内容管理系统最本质的区别:知识库的查询全部前置到构建期,换来的是零运维。代价也直接——改一个字的成本就是一次全量重建,几百篇还无所谓,上万条之后就得另想办法了。
v5 把插件搬出仓库,布局写进 YAML
Quartz v4 的插件是仓库里的源码目录,想改行为就得改代码。v5 换了思路:插件是 npm 包,配置是 YAML 声明。quartz.config.yaml 里每个插件长这样:
- source: "@quartz-community/graph"
enabled: true
layout:
position: right
priority: 10
source 指向 npm 包,options 传参数,order 决定它在构建管线里的处理顺序,layout 决定 UI 位置(left、right、beforeBody、afterBody、footer)加优先级。侧栏目录树、右侧图谱、反链、目录、工具栏都靠这几行排布,不用碰组件代码。我们这份配置 46 个插件条目,打开 36 个、关掉 10 个——关掉的条目留在文件里当备选,包装上也不启用。
插件安装是构建的前置步骤:package.json 里的 prebuild 钩子跑 npx tsx ./quartz/plugins/loader/install-plugins.ts,按配置里 enabled 不为 false 的条目去装 npm 包。所以克隆仓库后光跑 npm install 是不够的。
中文知识库的两个细节
中文字体是第一个容易翻车的点。theme.fontOrigin: systemFonts 配合 typography 指定字体栈:
typography:
body: "PingFang SC, Source Han Sans CN, Hiragino Sans GB, Microsoft YaHei, sans-serif"
不指定就是拉丁字体硬套中文,字重和行距都会难看。第二个细节是 frontmatter 里的中文键:note-properties 插件的 includedProperties 是白名单,我们开的是 description、tags、aliases,加 学科、重要度、场景、状态 四个中文属性名,Obsidian 里写的 YAML 直接渲染成笔记头部的属性卡,不会被当垃圾字段丢掉。
其他打开着的:KaTeX 数学(renderEngine: katex)、Mermaid、remove-draft(草稿不进构建)、unlisted-pages(不进列表但可访问)、canvas-page、bases-page、alias-redirects,以及 content-index——它顺带产出 sitemap 与 RSS。还有一个值得单独说:encrypted-pages 用 AES 在构建期加密指定笔记,迭代次数写在配置里(我们设的 600000),产物是 static/encryptedContentIndex.json。公开的 vault 里想留几篇私人笔记,不用另建一个库,也不用把它们排除在外。
五个真实踩坑
首页文件名是硬约束。 Quartz 只认 content/index.md,而 Obsidian 里首页习惯叫 00-首页.md,数字前缀是排序习惯。同步完 vault 不改名,线上首页就是一个空目录页。我们的发布脚本里固定有一行 mv content/00-首页.md content/index.md。
vault 直接当内容源,语义要想清楚。 发布流程第一步是 rsync 把 iCloud 里的 vault 同步到 content/,排除 .obsidian、.trash、.DS_Store。加上 --delete 才算让 vault 保持唯一真源,代价同样明确:删掉一篇笔记等于删掉一个页面,历史版本只能靠 git。
Node 版本卡得比较死。 engines 要求 node >= 22、npm >= 10.9.2,本机 node 26.3、npm 11.16 正常。
验证别只看首页 HTML。 左侧目录树是客户端渲染的,首页源码里抓不到 href——第一次用正则抓链接,拿到的是空数组,差点以为构建坏了。要看 static/contentIndex.json,或者构建完直接 curl 线上页面的 <title> 确认。
笔记的修改时间要靠 git 或 frontmatter。 「最后修改」由 created-modified-date 插件推断,我们的优先级设成 frontmatter → git → 文件系统。内容目录没被 git 跟踪时,构建会对每篇笔记打一行警告:isn't yet tracked by git, dates will be inaccurate——这次构建 151 篇全部命中,因为 content/ 还没提交。想让时间线可信,要么在 frontmatter 里写日期,要么把内容纳入版本管理,文件系统时间是最不可靠的兜底。
适合谁,不适合谁
适合:手里已经有 markdown 或 Obsidian 笔记库,想公开其中一部分;要双链、图谱、全文搜索这种阅读体验;不想维护数据库和后端;静态托管随便找,自己的服务器、GitHub Pages、Vercel 都能扔。
不适合:需要评论、登录、多作者协作、数据库查询的内容站(那是 Astro 那类框架加 SSR 的活);只需要一条时间线列表的博客;手里还没笔记、只想「搭个站」的场景——Quartz 的价值来自内容之间的链接密度,空库用不出效果。
快速上手
# 1. 环境:node >= 22
git clone https://github.com/jackyzha0/quartz.git
cd quartz && npm install
# 2. 内容:把 markdown 目录同步进 content/
# 首页必须是 content/index.md
# 3. 配置:quartz.config.yaml
# baseUrl: www.example.com
# locale: zh-CN
# 4. 本地预览(默认 8080 端口)
npx quartz build --serve
# 5. 构建:产物全在 public/
npx quartz build -c 1
-c 是并发数,默认吃满 CPU 核数,固定成 1 让构建日志和内存可控。我们把它包进了一个四步发布脚本:同步 vault、构建、打包上传、curl 线上标题验证,一条命令跑完,发布就不再是需要记得去做的事。
还有一件容易忽略的事:站上的链接密度不会自己变好,它和 vault 里一样。真正让知识库变厚的动作还是原来那三步——写文章、回填卡片、再发布。