为什么选这套方案
我想要的很简单:在 Obsidian 里写笔记,标记 publish: true,push 后自动变成博客。
调研了一圈,最终选择了:
- Obsidian — 本地 Markdown 编辑,双链、标签、模板一应俱全
- Quartz V4 — 专为 Obsidian 设计的静态站点生成器,原生支持 wikilinks、callouts
- Cloudflare Pages — 免费全球 CDN,私有仓库也能用
- GitHub Actions — 自动化构建部署
架构
两个 Private 仓库完全隔离,私有内容永远不会泄露:
[Private] my-vault (Obsidian vault)
↓ push 触发 GitHub Actions
[Private] my-blog (Quartz V4)
↓ 自动构建部署
[Live] my-blog.pages.dev
| 仓库 | 角色 | 存什么 |
|---|---|---|
| vault | 内容仓库 | Markdown 笔记、图片、附件 |
| blog | 引擎仓库 | Quartz 配置、构建流水线、主题样式 |
内容的 Single Source of Truth 是 vault,不是 blog。blog 仓库不应该对内容有任何”记忆”。content/ 是两者的交汇点——它只在 CI 的临时环境中被填充,用完即销毁。
核心机制:ExplicitPublish
Quartz 的 ExplicitPublish 过滤器是这套方案的关键。只有 frontmatter 包含 publish: true 的 Markdown 文件才会被构建,其余全部忽略。
// quartz.config.ts
filters: [Plugin.ExplicitPublish()]注意:这个过滤器只作用于 .md 文件。图片、PDF 等附件需要用 ignorePatterns 额外排除。
发布流程
整个流程由两个 GitHub Actions workflow 串联:
Vault 仓库 — trigger-blog.yml:push 到 main 时,向 blog 仓库发送 repository_dispatch 事件。
Blog 仓库 — deploy.yml:收到事件后,克隆 vault、复制内容到 Quartz 的 content/ 目录、构建、部署到 Cloudflare Pages。
关键是只复制需要发布的目录,而非整个 vault:
- name: Copy vault content to Quartz
run: |
mkdir -p ./content
rm -rf ./content/*
cp -r ./vault/index.md ./content/
for dir in Blog Knowledge Attachments; do
[ -d "./vault/$dir" ] && cp -r "./vault/$dir" ./content/
done从写完文章到上线,大约 1-2 分钟。
踩过的坑
1. Cloudflare Pages 项目要先创建
wrangler pages deploy 不会自动创建项目,需要提前通过 Cloudflare Dashboard 或 API 建好,否则报 Project not found。
2. Wrangler 的 --commit-dirty 和 --branch
CI 环境中 wrangler 检测到 uncommitted changes 会把部署当成 preview 而不是 production。需要加两个参数:
command: pages deploy public --project-name=my-blog --commit-dirty=true --branch=main3. index.md 必须在 vault 根目录
Quartz 的首页是 content/index.md。如果你把它放在子目录(比如 Blog/index.md),复制到 content/Blog/index.md 后首页会 404。解决方法是把 index.md 放在 vault 根目录,单独复制。
4. 不要把整个 vault 复制到 Quartz
早期用 cp -r ./vault/* ./content/ 会把 CLAUDE.md、README、docs/ 等私有文件都复制过去。虽然 ExplicitPublish 会过滤掉没有 publish: true 的 .md 文件,但非 .md 文件(如配置文件)不受过滤器保护。必须显式选择要复制的目录。
5. 不要在 .gitignore 中忽略 content/
这是最隐蔽的一个坑,直接导致了首页 404。
双仓库架构下,blog 仓库的 content/ 只在 CI 运行时被填充,本地始终是空的。很自然会想把它 gitignore 掉:
# 看起来很合理,实际会翻车
content/*
!content/.gitkeep但这会导致 Quartz 构建出 0 个页面。 原因是 Quartz 内部使用 globby 扫描 content/ 目录,并且默认开启了 gitignore: true:
// quartz/util/glob.ts
await globby(pattern, {
cwd,
ignore: ignorePatterns,
gitignore: true, // ← 这行是关键
})gitignore: true 意味着 globby 会读取仓库根目录的 .gitignore,把匹配的文件排除。content/* 会让 globby 忽略 content 下的所有文件——即使这些文件是 CI 运行时动态复制进去的,根本不受 Git 追踪。
实际表现:CI 构建日志显示 Found 0 input files from content,Quartz 输出一个空壳网站(只有 CSS/JS/404 页面),首页 404。
解决方案:不要在 .gitignore 中提及 content/。CI 在 runner 的临时环境中操作 content/,这些文件从来不会被 git add,不需要 gitignore。本地开发时手动复制 vault 内容到 content/ 做预览,用完后清理即可。
延伸教训:.gitignore 不只是 Git 在用。globby、ripgrep、fd、ESLint 等工具都会读取它。当你在 .gitignore 里写下一条规则,你可能同时在告诉构建工具”别处理这些文件”。经验法则:如果一个目录的内容需要被构建工具处理,就不要把它放进 .gitignore。
iPhone 同步:iCloud 方案
搭好桌面端之后,自然想在手机上也能看笔记。Obsidian 在 iOS 上支持 iCloud 同步,配置很简单。
原理
把 vault 目录移到 Obsidian 的 iCloud 容器里,在原位置放一个软链接:
~/my-vault → symlink → ~/Library/Mobile Documents/iCloud~md~obsidian/Documents/my-vault
Mac 端通过软链接访问,路径不变,Git 照常工作。iPhone 上 Obsidian 通过 iCloud 自动同步。
操作步骤
# 1. 移到 iCloud Obsidian 目录
mv ~/my-vault ~/Library/Mobile\ Documents/iCloud~md~obsidian/Documents/my-vault
# 2. 在原位置创建软链接
ln -s ~/Library/Mobile\ Documents/iCloud~md~obsidian/Documents/my-vault ~/my-vault然后在 iPhone 的 Obsidian 里创建 vault 时选择 Store in iCloud,就能看到 vault 了。
注意事项
- iCloud 会同步
.git目录,但只要只在 Mac 上执行 Git 操作,不会有问题 - 大文件(图片、PDF)同步可能需要几秒到几分钟
- 如果遇到冲突文件,以 Mac 端为准
最终效果
在 Obsidian 里写笔记,设 publish: true,push。等一两分钟,文章就在线了。手机上也能随时翻阅所有笔记。
整套方案的维护成本几乎为零——不需要管服务器,不需要手动构建,写完就发。