从零到上线:Quartz 5 博客搭建全过程总结

什么是 Quartz

Quartz 是一个开源的静态网站生成器,官方对自己的定义只有一句话:

Quartz is a set of tools that helps you publish your digital garden and notes as a website for free. (一套工具,帮你把数字花园和笔记免费发布成网站。)

作者是 Jacky Zhao(jzhao.xyz),"数字花园(digital garden)"这个词的流行和他那篇《Networked Thought》关系很大。本仓库当前版本是 Quartz 5(5.0.0),主分支固定叫 v5。

几个必须说清的性质:

  • 它不是一个托管服务,也不是 CMS。 它只做一件事:把 content/ 里的 Markdown 编译成 public/ 里的纯静态 HTML / CSS / JS。
  • 产物是纯静态文件,扔给任意静态托管就能跑:Cloudflare Pages、GitHub Pages、Netlify、Vercel,甚至自己一台机器。
  • 技术栈是 Node.js(要求 ≥ 22)+ TypeScript,构建走 esbuild,配置就是仓库根目录一个 quartz.config.yaml。
  • 插件式架构,本仓库用的 obsidian 预设一共有 46 个插件条目,功能开关全在 YAML 里,不用改代码。

链接

项目 地址
官方文档(从这开始) https://quartz.jzhao.xyz/
官方仓库 https://github.com/jackyzha0/quartz
「数字花园」概念原文 https://jzhao.xyz/posts/networked-thought
演示站点 https://quartz.jzhao.xyz/
本站(本文实测对象) https://blog.buzhidao.de5.net/
本站源码仓库(私有) https://github.com/buzhidao10068/Quartz
评论数据仓库(giscus) https://github.com/buzhidao10068/Quartz-comments
Quartz 是干什么的

把本地笔记库变成一个带搜索、带图谱、能评论的网站,而笔记本身一个字都不用改。

具体它能干的:

① 原样发布 Obsidian 笔记 [[双链]]、#标签、> [!note] callout、Mermaid 图、脚注、表格、任务框、.canvas、.base 全都原生认。hard-line-breaks 还能让"单个换行当 <br>",和 Obsidian 里的观感一致。

② 一个像样的站点该有的导航能力 全文搜索(浏览器端索引,不依赖服务端)、探索栏、关系图谱、反向链接、悬浮预览、目录(TOC)、暗色/亮色切换、多语言界面(内置 zh-CN)。

③ 细粒度的发布控制 draft: true 干脆不生成 HTML;unlisted: true 生成页面但不进列表/搜索/图谱/RSS;explicit-publish 插件可以反过来做成白名单,只有写了 publish: true 的才发;encrypted-pages 还能给单页加密码。

④ 自动生成一堆站点级产物 RSS(index.xml)、sitemap.xml、搜索索引 JSON、favicon、分享卡片图(OG image,用 satori + sharp 在构建时生成)、GitHub Pages 用的 CNAME。

⑤ 什么都不用运维 推一次代码部署一次。没有数据库、没有服务端进程、没有证书要手动续——静态托管把这些全包了。

它不干的事(容易误解的地方):

  • 不管评论后台。评论靠 giscus 接 GitHub Discussions,数据存在评论仓库里,Quartz 只负责把评论区嵌入页面。
  • 不管托管和域名。域名、HTTPS、CDN 都是托管平台的事。
  • 没有在线编辑器。写作始终在 Obsidian / 任意 Markdown 编辑器里,Quartz 只在"从 Markdown 到网站"这一段工作。
一、建仓:走「选项 A」

官方安装文档给了两条路:

做法 origin 指向 后患
选项 A(推荐) GitHub 页面点 Use this template → Create a new repository 你自己的仓库 几乎没有
选项 B git clone 官方仓库 jackyzha0/quartz 之后必须手动改 remote,否则 push 打回官方仓

选项 A 省掉的正是最容易出错的一步:不用再折腾 git remote set-url origin。建完确认默认分支是 v5 —— Quartz 5 全链路钉死在这个分支名上(CLI 里 QUARTZ_SOURCE_BRANCH = "v5"、CI 触发分支、Cloudflare 的 Production branch 全都是它)。

git clone https://github.com/<你的用户名>/<仓库名>.git
cd <仓库名>
二、装依赖 + 初始化
npm i
npx quartz create

前置条件别忽略:Node.js ≥ 22、npm ≥ 10.9.2。仓库的 .npmrc 开了 engine-strict=true,版本不够时 npm i 会直接硬失败,而不是给个警告。

npx quartz create 会依次问四件事,我的选择:

问题 我选的 理由
模板 obsidian 笔记库直接搬过来,硬换行、双链、标签全对齐 Obsidian 行为
content 策略 new(自己写) 后来直接在 content/ 里写;要做软链得先开 Windows 开发者模式
baseUrl blog.buzhidao.de5.net 不带 https://,尾部斜杠也会被自动剥掉
链接解析 shortest 和 Obsidian 的 [[页面名]] 写法一致

等价的一行非交互写法:

npx quartz create --template obsidian --strategy new --baseUrl blog.buzhidao.de5.net

[!danger] create 是整份覆盖 quartz.config.yaml
不是合并,官方文档原话就是 “always overwrites”。所有手改过的 locale、字体、评论参数都会被打回模板默认值。已经改过配置的话,先备份。

四个模板的差别其实很小(我把三份 YAML 逐条 diff 过):

模板 条目数 相对 default 的差别
default 45 基准
blog 45 +comments、recent-notes;−encrypted-pages
obsidian 46 +hard-line-breaks、@quartz-themes/core;+Excalidraw
ttrpg 47 =obsidian + Leaflet 地图(git 来源)

也就是说,"blog 模板"的全部含义就是多开了评论区和一个"最近的笔记"侧栏。想要 blog 风格不用重跑 create,只要把这两条 enabled: false 改成 true 就行 —— 这也顺便躲开了配置被覆盖的风险。

三、中文化

Quartz 内置 zh-CN 翻译,改一行就够:

configuration:
  locale: zh-CN

字体是另一个要注意的点:默认排版用的是 Schibsted Grotesk / Source Sans Pro,不含中文字形,中文会回退到系统字体(Windows 上就是微软雅黑),中英文风格不统一。

  theme:
    fontOrigin: local        # 不依赖 Google Fonts,大陆访问更稳

fontOrigin 只有 googleFonts 和 local 两个合法值。选 local 就直接吃系统字体,不请求外部 CDN;想让中英文统一,可以换 Noto Sans SC,但那就得走 Google Fonts 或者自己托管字体文件。

四、本地预览
npx quartz build --serve --port 3005 --wsPort 3006
  • --port 默认 8080,我习惯用 3005;
  • 热重载走 WebSocket,默认占 3001,被占用时加 --wsPort 3006;
  • --serve 隐含 watch,改 Markdown 自动重建并刷新浏览器。
五、我这台机器专属的坑

这一节是我实际撞上的,Quartz 本身没问题,纯粹是环境。如果你也在 Windows + 沙箱终端里跑,可能会遇到同款。

坑 1:mkdir quartz\.quartz-cache: Access is denied

构建一开始就死:

✘ [ERROR] Failed to create output directory:
  mkdir ...\quartz\.quartz-cache: Access is denied.

根因排查了很久,最后定性为 Windows 完整性级别(MIC):工作区根目录被打上了会向下继承的 Low 标签,于是根目录下新建的 node_modules 也继承 Low,里面每次启动的 esbuild.exe 都只能是 Low 级别;而 clone 时就存在的 quartz\、docs\ 仍是默认 Medium —— Low 进程写 Medium 目录被 Windows 的"禁止向上写"拦掉。

两个绕法,任选一个:

# A. 把 node_modules 的完整性级别提回 Medium(几千个文件,几秒完成)
icacls node_modules /setintegritylevel "(OI)(CI)Medium" /T
# B. 把 esbuild.exe 复制到临时目录,用环境变量指向它
$tmp = "$env:TEMP\esbuild\esbuild.exe"
Copy-Item node_modules\esbuild\esbuild.exe $tmp
$env:ESBUILD_BINARY_PATH = $tmp

[!warning] 这个坑的关键特征
换成"完全权限"跑也修不好,重装 node_modules 也修不好 —— 因为新目录仍然继承 Low。别在提权上浪费时间,直接动完整性标签。

坑 2:npx quartz ... 卡住直到 OOM

npx quartz --version 都可能几分钟没有任何输出,最后 FATAL ERROR: Ineffective mark-compacts near heap limit。

判据很明确:崩之前一行 quartz 输出都没有,说明崩在启动之前,不是构建吃内存。原因是 npx 缓存里给这个包建了一个指向仓库根的 Junction,每次校验都要把几千个文件、两百多兆走一遍。

绕过方式:直接调入口脚本,完全等价。

node ./quartz/bootstrap-cli.mjs build --serve --port 3005

(npm cache clean --force 也能修,但要重新下载。)

坑 3:--serve 模式下样式和脚本全 404,页面变成单列

页面能打开,但布局是一列、搜索框点不动、探索栏空白。DevTools 里能看到 index.css、postscript.js 全是 404。

原因是资源命名不一致:--serve 用不带哈希的名字(index.css),生产构建用带哈希的名字(index-bb6c1746.css)。如果 public/ 里残留了生产构建的产物,serve 模式引用的那三个文件就不存在。而写盘异常在 emit 阶段是被静默吞掉的,所以不报错。

更隐蔽的是:主样式表丢了 = 没有 #quartz-body 的网格规则 = 页面永远单列,跟窗口宽度无关。我一开始误判成"窗口太窄",绕了一大圈 —— 排查时先看 Network 里 index.css 是不是 200,能省很多时间。

修复:

# 先 Ctrl+C 停掉 dev server,然后
Remove-Item public -Recurse -Force
npx quartz build --serve --port 3005

顺手记一条纪律:开着 dev server 时不要另跑一次 npx quartz build(生产构建会重写 public/ 里的资源名,两边打架)。要单独验证构建,可以输出到别的目录:npx quartz build -o .build-check。

坑 4:quartz.lock.json 是空的,还以为是坏了

{ "version": "1.0.0", "plugins": {} }

这是正常的。lockfile 只登记 git 来源的插件(github:owner/repo、git+https://…、本地路径);Quartz 5 绝大多数插件是 npm 包,版本由 package.json + package-lock.json 管,不进 lockfile。所以纯 npm 配置下 .quartz/plugins/ 是空的、lockfile 也是空的,都对。

六、评论:giscus

Quartz 的 comments 插件只支持 giscus 一种 provider,而 giscus 的两个硬要求容易踩:

  1. 评论仓库必须是 public。站点源码可以私有,但评论要单独开一个公开仓库放 Discussions。
  2. 仓库要开启 Discussions,并装 giscus App。

我的配置:

  - source: "@quartz-community/comments"
    enabled: true
    options:
      provider: giscus
      options:
        repo: buzhidao10068/Quartz-comments
        repoId: R_kgDOU74DEQ
        category: Announcements
        categoryId: DIC_kwDOU74DEc4DHDSJ
        mapping: pathname      # 关键
        strict: true
        reactionsEnabled: true
        inputPosition: bottom
        lightTheme: light
        darkTheme: dark
        lang: zh-CN
    layout:
      position: afterBody
      priority: 10

三个值得说的选择:

① mapping: pathname 而不是默认的 url。 评论跟"页面路径"绑定,不跟域名绑定。我从 blog2.buzhidao.de5.net 换成 blog.buzhidao.de5.net 时,评论一条没丢 —— 因为 /notes/abc/ 这个键没变。用 url 的话,域名一改就分成两套,旧评论等于清零。

② category: Announcements。 这个分类默认只有维护者能新建讨论,可以防止别人在你仓库里乱建帖子;giscus bot 有权限,不受影响。

③ 评论数据不在你的站点仓库里。 它存在评论仓库的 Discussions 里 —— 一个页面对应一条 Discussion,一条评论就是一条回复。GitHub 就是你的评论后台,回复、删除、锁定都在那里做。

[!note] 两条使用提醒

  • 没人评论时 Discussions 是空的,这很正常 —— 第一条评论发出时 giscus 才会创建对应讨论。
  • 本地 localhost 测试时别真发评论:pathname 映射下,本地 / 和线上首页的 / 是同一条讨论。
七、部署到 Cloudflare Pages

从 GitHub 拉代码构建,推一次部署一次。

配置项 值
Production branch v5
Framework preset None
Build command git fetch --unshallow && npx quartz plugin install --from-config && npx quartz build
Build output directory public
环境变量 NODE_VERSION = 24

构建命令里两段前缀都不是可选的:

  • git fetch --unshallow —— Cloudflare 默认浅克隆,不加这行 created-modified-date 从 git 取日期会不准;
  • plugin install --from-config —— 按配置补装缺失插件。注意它只处理 git 来源的插件,npm 插件靠构建前的 npm ci 自动装好。而且装失败只打警告、不会让构建失败,所以构建成功 ≠ 插件生效,日志里的 :warning: 要自己看一眼。

绑自定义域名

Pages 项目 → Custom domains → Set up a domain → 填域名。分两种情况:

  • 域名 DNS 已在 Cloudflare:自动加记录、自动签证书,直接生效。apex 域名(example.com)只能走这条,Cloudflare 会用 CNAME 展平。
  • DNS 在别处:子域名加一条 CNAME 指向 <项目名>.pages.dev;apex 域名不能直接写 CNAME,要么把 nameserver 迁到 Cloudflare,要么看服务商是否支持 ALIAS/ANAME。

[!warning] 顺序不能反
必须先在 Pages 面板关联域名,再去加 DNS 记录。只在 DNS 里手动加 CNAME 会得到 522。

域名换好后,quartz.config.yaml 里的 baseUrl 要同步改(不带协议、不带尾斜杠)——它决定 sitemap、RSS、分享卡片图(OG)和 giscus 主题 CSS 的地址。

旧域名建议保留并做 301:RSS 订阅者订阅的是具体地址,不会自动跟着换域名

八、日常写作流程
# 1) 在 Obsidian 里写(把 content/ 直接当 vault 打开,我是打开content的父文件夹)
# 2) 本地预览
npx quartz build --serve --port 3005 --wsPort 3006
# 3) 发布
npx quartz sync -m "content: 新增文章标题"(发布就是git操作了)

sync 的动作是 git add -A + commit + pull + push。默认提交信息是 Quartz sync: <时间戳>,想写自己的就加 -m。

[!danger] sync 是强推,而且推的是"当前分支"
代码里是 git push -uf origin $(git rev-parse --abbrev-ref HEAD),但 pull 固定走 origin v5。所以跑之前一定确认 git branch --show-current 是 v5,否则会变成"拉 v5、强推别的分支"。

每篇文章的 frontmatter

---
title: 文章标题
description: 一句话摘要,用于搜索和分享卡片
tags: [Quartz, 博客搭建]
created: 2026-10-26
modified: 2026-10-26
published: 2026-10-26
enableToc: true
comments: true
draft: false
unlisted: false
password:
stealth: false
publish: true
数字编号: 
---

几个容易踩的点:

字段 注意
publish 如果开了 explicit-publish 插件,不写 publish: true 就完全不会生成页面 —— 这是最容易"文章消失了却找不到原因"的一处
tags 必须写成数组(tags: [a, b]),写成单值会让"有标签才显示"这类条件失效
日期 只认 created / modified / published,Obsidian 常见的 date: 字段 Quartz 不读
draft vs unlisted draft: true 连 HTML 都不生成;unlisted: true 生成页面但不进列表/搜索/图谱,直链能访问
大小写 enableToc、cssclasses、socialImage 写成 enable_toc / CSSClasses 都不生效

九、最终成果

项目 结果
站点 静态 HTML,托管在 Cloudflare Pages
域名 自定义域名 + 自动 HTTPS
界面语言 zh-CN,日期按中文格式化
评论 giscus,数据存 GitHub Discussions,换域名不丢
写作方式 Obsidian 直接写 Markdown,content/ 当 vault
运维成本 0(推代码即部署)
花费 0

同步自我的blog:https://blog.buzhidao.de5.net/quartz-5相关/从零到上线:quartz-5-博客搭建全过程总结

1 个赞

和 hexo. Hugo 是不是差不多

我不知道:sob:没用过Hugo