把個人知識庫建起來並公開發布(Quartz v5 + Cloudflare Pages)

我要的東西很具體:一個能邊學邊記、記完就自然變成公開站點的地方,不要訂閱費、不要自己維護伺服器、不要依賴任何公司的雲端。最後落在 Quartz + Cloudflare Pages 上,整條路徑零費用。

這篇是搭建手册,但我把我判斷錯的地方也寫進去 —— 那些才是這篇有用的部分。

為什麼是這套組合

選擇理由我排除掉的替代方案
Markdown 純文字作為唯一真相源十年後還能讀、能 grep、能被任何工具處理Notion:資料在別人的資料庫裡,導出困難,且兩個真相源的同步終究會殺死個人知識庫
Obsidian 只做編輯器離線可用、雙向連結體驗好、不锁定格式另建一個 Obsidian vault 再同步到 content:不要這樣做,直接讓 content/ 本身成為 vault
Quartz靜態站、支援 wiki-link、圖譜、搜索、後端链接都是内置自己寫生成器:得不到的東西遠多於省下的時間
Cloudflare Pages免費、push 即部署、自帶全球 CDN 與 HTTPS自建伺服器:多一個要patch的東西,而且我上傳帶寬再大也不該拿去當公網服務

一、起手:用 template,不要 fork

Quartz 的仓库有 Use this template。點它,生成一個屬於你的、只有一個初始提交的私有仓库,再 clone 下來。

我 fork 過一次,後來改掉了。差別很實際:fork 帶著上游幾百個提交,之後每次 git log、每次「最後修改時間」的計算、每次跟上游比對都要穿過那堆不属于你的歷史。而你要跟进上游的能力,本來就不是靠 fork,是靠讀它的 changelog。

預設分支是 v5,不是 main。這一件小事会在后面部署时咬你一次(見第五節)。

npm i
npx quartz create

npx 跑的是本项目 node_modules/.bin 里的可执行文件,不需要全局安装任何东西。quartz create 会生成 content/index.md 與 quartz.config.yaml。

二、v5 最大的差異:配置只有一份 YAML

v4 的配置是 TypeScript(quartz.config.ts)。v5 換成了單一的 quartz.config.yaml,插件也從「內建模組」變成「可插拔單元」。

这里我踩了第一個誤判:仓库里同時存在 quartz.config.default.yaml。那份是上游預設值,不要改它,你的配置只寫在 quartz.config.yaml。

我實際改的三項:

鍵我原本的值改成為什麼
configuration.baseUrlgithub.com/<倉庫路徑>.git<你的子域名>.pages.devquartz create 從 git remote 自动抓的,抓錯了:baseUrl 要填網站域名,不是仓库地址。它影響 sitemap、RSS、canonical 與 OG 標籤
configuration.localeen-USzh-TW界面文字(搜尋佔位符、404、日期格式)
configuration.pageTitleQuartz 5自己的站名瀏覽器標籤與首頁標題

baseUrl 不带 https://,也不带首尾斜杠 —— 這是上游文件明写的约定。

還有一項得手动清:footer 插件的 options.links 預設硬編碼了上游作者的仓库與 Discord。你的站點頁腳不該挂著別人的連結。

三、第二個誤判:以為插件要自己裝

v5 的功能全在插件裡(搜索、圖譜、後链、目录树……v4 是內建的),於是我看到 .quartz/plugins/ 是空的、quartz.lock.json 不存在,就下了結論:「插件沒装,站點會缺功能」,還準備了一條安装命令。

這條結論是錯的。 實際跑下去报錯,我才去讀 docs/cli/plugin.md 與 package.json:

  • 配置裡 source 写成 @quartz-community/xxx 這種以 @ 開頭的字串,是 npm 依賴,它们在 package.json 的 dependencies 里,npm i 就跑完了 —— 你的站点里大部分插件早就装好了
  • .quartz/plugins/ 與 quartz.lock.json 只服務於从 git 源装的那類插件(npx quartz plugin add github:用戶/仓库)
  • 而且 .quartz/ 在 .gitignore 裡,所以它在你的仓库裡本來就應該是空的 —— 那不是缺陷,那是設計

這條誤判值得記

「我預期它是空的」與「它是空的所以我得修」是兩件不同的事。空目錄有兩種:待填的、與本就不該有東西的。 判据只能来自代碼或文件,不能来自直覺。

四、中文搜索不需要任何配置

早期版本裡「要給 Quartz 装 CJK 分詞」那一步作廢。Quartz 核心對中文是逐字分詞(character-by-character tokenization),内建行為。我是在仓库的測試檔里看到那条测试用例名字才確信的 —— 讀測試比讀教程可靠,因為测试是會被執行到的文檔。

五、目錄結構,與一條關於機密的規則

content/
  index.md        ← 首頁
  payments/       ← 公開標準可推导的領域知識
  integration/    ← 結算檔案、對帳、MQ、批次、冪等
  spring/         ← 企業級框架主線
  database/       ← MySQL、索引、執行計畫、事務隔離
  nodejs/         ← 副線
  ops/            ← 部署基本功、排障方法論(本檔所在)
  android/        ← 本職
  ai-notes/       ← 與 AI 對話裡「自己不會的知識點」的落點
  private/        ← 🔴 永不構建、永不提交
  templates/      ← 筆記模板

content/private/ 是這篇最重要的一節。

quartz.config.yaml 的 ignorePatterns 預設含 private,.gitignore 也含 private/。用途很直白:公開站點的讀者包含搜尋引擎、獵頭與雇主,他們不是同一群人。 我給自己定的判斷標準只有一句:

這段內容能不能從公開規範文檔推得出來? 推不出来就不要寫到公開區。判斷不了就丟 private/ —— 以後可以往外搬,反過來收不回來。

而這個默認值我没有相信,實測了四遍才算数:

  1. git check-ignore 確認 private/ 真被忽略
  2. 在 content/private/ 放一個只含標記字串的檔案(內容就是隨機字元加一個不可能出現在正常文章裡的詞)
  3. npx quartz build,然後在 public/ 里 grep 那個標記 —— No files found
  4. 直接訪問 public/private/ —— 目錄根本不存在

這四步花不了五分鐘,但它撑起「我可以安心寫筆記」整個前提。對安全邊界,默認值必須被實測推翻或確認,不能被他讀過就算。 验完記得把 canary 檔删掉。

六、本地驗證

npx quartz build --serve

build 把 content/ 編譯進 public/;--serve 顺手起一個本地 HTTP 伺服器(不加就只生成檔案、不起服務)。開 http://localhost:8080,看完 Ctrl+C。

我的驗收清單(每項都是能客觀判定的):

  • 搜中文關鍵詞能命中文章
  • 雙向連結可跳轉、圖譜與後链有内容
  • 頁面原始碼裡 lang="zh"、標題是自己填的站名、頁腳沒有作者的連結
  • 手機连同 WiFi 能打開

最後一項常被省略,但它测的是「生成的資源有絕對路徑依賴嗎」—— 這類問題只在别的機器上才露出來。

七、提交前:先看清楚要推什麼

我差點漏掉的一件事:quartz.config.yaml 是新檔案,沒有 git add 就不會进仓库。 後果不是本地壞(本地照樣能跑),而是雲端 clone 時拿不到它,退回上游預設配置 —— 站点會構建成功,但你的 baseUrl、locale、站名全部失效。

「本地好、雲端壞」最常見的成因

不是腳本寫錯,是生成物或被忽略目錄的差別。.quartz/、node_modules/、public/ 都是 gitignore 的,所以每次雲端构建都要重新產生。這也解释了下一節的构建命令為什麼那麼長。

git add -A
git status
git commit -m "配置站點並寫入首批筆記"
git push

git status 要在 commit 之前跑,不是事後看。我用的兩個客觀判据:

  • git ls-files quartz.config.yaml 必須有輸出(证明配置真进了仓库)
  • git ls-files content/private 必須為空

如果 git status 裡出現 public/、node_modules/ 或 content/private/ 的任何檔案,表示忽略規則没生效,停下來先查 —— 後者尤其嚴重。

八、Cloudflare Pages:參數與那個必須额外設的變量

選項值含义
Production branch你的預設分支(我的是 v5,不是 main)监聽哪個分支的 push。填錯就是 push 了不構建
Framework presetNone按純静态檔案處理
Build commandgit fetch --unshallow && npx quartz plugin install --from-config && npx quartz build見下方拆解
Build output directorypublic只發布這個目錄,它是純生成物

Build command 三段用 && 串起來(前一条成功才執行后一條):

  1. git fetch --unshallow —— Cloudflare 默认浅克隆(只抓最近幾個提交)。而 Quartz 的「最後修改時間」是靠 git log 算的,不補完整歷史,每篇文章的日期都會顯示成構建當天。
  2. npx quartz plugin install --from-config —— 因為 .quartz/ 不在仓库裡,雲端每次都得重建。--from-config 的意思是「以 quartz.config.yaml 为准装」(預設行為是读 quartz.lock.json,而那個檔案在 gitignore 目錄裡,雲端没有)。
  3. npx quartz build —— 生成站点。

官方文件沒列、但你的仓库會逼你失敗的一項:Environment variable NODE_VERSION = 22。

理由能从他自己的仓库查出來,不是猜的:

  • .npmrc 第一行是 engine-strict=true
  • package.json 的 engines 要求 node: ">=22"

engine-strict=true 的意思是 「Node 版本不符就直接終止安装」,不是警告。構建機的預設 Node 偏舊,不设這個變量会在 npm install 階段當場失败。你本地是新 Node 所以永遠看不到這個問題 —— 雲端是另一台機器。

授權 GitHub 時只勾這一個仓库,不要給全部。

九、上線後複驗(這一步才是真的驗收)

  • 無痕窗口打開线上 URL,搜你的敏感字串(真實公網 IP、MAC、機構名稱、標記字串),搜不到才算過
  • 线上直接訪問 <站點>/private/<檔名> —— 必須 404
  • 手機用行動網路(不連家裡 WiFi)能打開 —— 证明真的上了公網
  • 关掉本地 --serve

前三項都不是「應該沒問題」的層級。公開發布之後才發現要撤,成本是無限大,因為搜尋引擎已经爬走了。

十、花銷

0 元。 Cloudflare Pages 免費層、.pages.dev 子域名自带、不需要注册域名、不需要伺服器。這一路唯一要付錢的環節(買域名、上访问控制)我暫時都不需要。

幾個我會回頭補的洞

  • 站點訪問控制:目前靜態站點人人可讀。只要開始寫任何可能敏感的內容,它就從「以後再說」变成前置條件。
  • private/ 的備份:它被 gitignore,所以不進仓库、沒有异地備份。丢了損失最大的恰恰是這部分。得单独安排(另一個私有仓库,或對象存儲)。
  • 真正的異地備份:原始筆記才需要備份,public/ 可以随時重建。

相關

排查背後的思路 —— 一個假設是怎麼被實測推倒的:家用網路瓶頸排查:從「打不開」到跑滿千兆