Vincent5588 Wiki 公開站部署紀錄
部署日期:2026-05-03 線上網址:https://vincent5588-wiki.pages.dev GitHub repo:https://github.com/Vincent5588/vincent5588-vault 核心架構:iCloud vault → Gateway → GitHub → Quartz build → Cloudflare Pages
0. 版本歷程
| 版本 | 日期 | 主要變動 |
|---|---|---|
| v1.0 | 2026-05-03 | 初版建立。完成「Phase 0–9」一次性 setup(GitHub repo / 本機 git / gsync 腳本 / Cloudflare Pages / API token / GitHub secrets / deploy.yml + Quartz config / 第一次 build)。寫滿 §1–§9 主文 + §附錄 A–E |
| v1.1 | 2026-05-03 | layout 修復事件後續,當天連續踩 4 雷後沉澱:(a) §雷 8 新增「custom.scss 沒 @use \"./base.scss\" → 整 layout 死」(含 A/B diff 證據表 + 對 IT Wiki 部署啟示);(b) §雷 9 新增「deploy.yml 飄離部署紀錄」(含 3 次 doc-vs-code drift 對照表 + 短期/長期解法);(c) §附錄 B 同步至實際 deploy.yml 現狀,每段重要 step 加 # NOTE 註腳指向 §雷 章節;(d) §附錄 E 從「列重點 bullet」改寫為「完整 SCSS 內容 + 反向警告區塊」 |
| v1.2 | 2026-05-03 | gsync 砍 .github/ 事件——day-one bug 終於被發現:(a) §雷 10 新增「gsync rsync --delete 沒排除 .github/,每次 sync 都把 GitHub Actions workflow + Quartz config 砍掉,整個 deploy pipeline 一直在運氣好沒被觸發狀態下苟活」(含偵測過程 / 還原 script / 為什麼 v1.0 寫的時候沒抓到 / 跟規則 G 的關係);(b) §附錄 A 修正——rsync 加 --exclude='.github/',移到 --exclude='.git/' 上面(兩個 .git* 系列相鄰),加 # NOTE 指向 §雷 10。發現契機:Cowork session 跑完 gsync 後 GitHub Actions 沒觸發,逐層往前查發現 ~/vincent5588-git/.github/ 整個被刪 |
| v1.3 | 2026-05-03 | 修 gsync 後第一次重跑就踩 §雷 11:rsync 撞 .smart-env/ 內含 → ? 等特殊字元 filename → “Illegal byte sequence” + “utimensat: No such file” 整個 abort。一次補齊 3 個隱藏目錄 excludes:(a) §附錄 A 加 .smart-env/(Smart Connections plugin cache)/ .claudian/(Claudian plugin cache)/ .claude/(Claude Code 本機設定+token)三條 --exclude=;(b) # NOTE 擴成 4 群分類(各組為什麼必須排);(c) outputs/Fix gsync excludes.command — 雙擊修 user 本機 gsync。規律強化:規則 F 廣義版第 (b) 條「每加一個新類型的檔到目標位置,必同步更新 exclude list」第 1 個正面案例 |
| v1.4 | 2026-05-03 | Quartz v4.5.2 不支援 Obsidian Dataview runtime → vault 內 70+ dataview block 在線上站全變 raw code block。修法:寫 build-time Python converter `dataview_to_md.py`,在 deploy.yml 加 step 把 dataview block 編譯成靜態 markdown table。(a) 新增 §雷 12「Quartz 不支援 Dataview runtime」(含 converter 設計取捨:支援 TABLE/LIST/FROM/WHERE/SORT/LIMIT 共 ~46 blocks / 不支援 GROUP BY/FLATTEN/DataviewJS/TASK 共 ~26 blocks 留 placeholder);(b) §附錄 F 加 dataview_to_md.py 完整 source;(c) §附錄 B deploy.yml 加 Convert Dataview blocks step(在 Copy vault content 跟 Register custom plugin 之間);(d) outputs/Install Dataview converter.command — 雙擊裝到 user 本機 ~/vincent5588-git/.github/。靜態 table 用 | col | col | 標準 markdown,wiki-link 仍用 [[basename]] 由 Quartz render |
| v1.5 | 2026-05-03 | 新增 §4.3「Vault → Quartz 過濾規則」總表 —— 兩段 filter pipeline 視覺化(vault → git repo → Quartz build copy)+ Stage 1 / Stage 2 excludes 分類表(v1.0~v1.3 演進)+ 「會上線」目錄風險評估表(11 項,附建議行動)+ 候選排除清單(4 紅 / 1 黃 / 3 綠待 user 慢慢 review)+ 修法 SOP(哪段加 / 加完同步紀錄)。觸發契機:使用者問「過濾規則是什麼?哪些檔會公開?」—— 規範缺口;§4 客製化清單原本只列「沒做的功能」,沒寫過濾規則 |
同步紀律:本檔每次有意義改動(新增章節 / 改 §附錄 / 修步驟 / 加雷 X)都必須在本表加一行 row,跟
[[CLAUDE]]§0 版本歷程同模式。frontmatterupdated:是粗粒度信號,本表才是細粒度差異紀錄。詳見 CLAUDE §10 規則 H。
1. 架構全景
┌────────────────────────────────────────────────────────┐
│ 寫手側(你) │
│ │
│ ~/Library/.../iCloud.../Vincent5588/ │
│ └─ Obsidian 編輯 + Claude 對話 │
│ └─ 存檔(iCloud 自動跨裝置同步) │
└────────────────────────┬───────────────────────────────┘
│
│ 你手動跑 gateway sync
▼
┌────────────────────────────────────────────────────────┐
│ 你 Mac 本機 │
│ │
│ ~/scripts/vincent5588-gateway-sync.sh │
│ ├─ rsync iCloud vault → ~/vincent5588-git/ │
│ ├─ 過濾 .obsidian / .trash / *conflict* 等 │
│ ├─ 偵測敏感詞(password / api_key 模式) │
│ └─ git add + commit + push origin main │
│ │
│ ~/vincent5588-git/ ← 本機 git repo(cleansing 後 vault)│
└────────────────────────┬───────────────────────────────┘
│ git push
▼
┌────────────────────────┐
│ GitHub repo (private) │
│ Vincent5588/ │
│ vincent5588-vault │
└──────────┬─────────────┘
│ push 觸發
▼
┌────────────────────────────────────────────────────────┐
│ GitHub Actions(雲端 build) │
│ │
│ .github/workflows/deploy.yml │
│ 1. Checkout vault │
│ 2. Setup Node.js 22 │
│ 3. Clone Quartz 4 engine │
│ 4. npm install Quartz 依賴 │
│ 5. 套用客製 quartz.config.ts + custom.scss │
│ 6. 註冊 draftWarningBanner.ts plugin │
│ 7. rsync vault → quartz-engine/content/ │
│ 8. 加 _redirects 檔(/ → /wiki/) │
│ 9. npx quartz build → quartz-engine/public/ │
│ 10. cloudflare/pages-action 部署 │
└────────────────────────┬───────────────────────────────┘
│ API 推靜態檔
▼
┌────────────────────────┐
│ Cloudflare Pages CDN │
│ vincent5588-wiki │
│ .pages.dev │
└──────────┬─────────────┘
│
▼
全網讀者(含手機)
✅ HTTPS 自動
✅ 全球 CDN
✅ 0 server 維運
✅ 0 月費
2. 一次性 setup 步驟(已完成)
2.1 GitHub repo
1. 建 private repo: github.com/Vincent5588/vincent5588-vault
2.2 本機 git repo + 第一次 import
mkdir -p ~/vincent5588-git
cd ~/vincent5588-git
git init
# rsync iCloud vault → 本機 git repo
rsync -av \
--exclude='.obsidian/workspace*' \
--exclude='.obsidian/cache' \
--exclude='.obsidian/snippets' \
--exclude='.trash/' \
--exclude='.DS_Store' \
--exclude='*conflict*' \
--exclude='wiki/_skill-staging/' \
--exclude='wiki/_review-queue/' \
--exclude='wiki/reports/' \
--exclude='wiki/dist/' \
--exclude='private/' \
"$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents/Vincent5588/" \
~/vincent5588-git/
# .gitignore
cat > .gitignore << 'EOF'
.obsidian/workspace*
.obsidian/cache
.obsidian/snippets
.trash/
.DS_Store
*conflict*
wiki/_skill-staging/
wiki/_review-queue/
wiki/reports/
wiki/dist/
private/
node_modules/
.quartz-cache/
public/
EOF
# 第一次 commit + push
git add .
git commit -m "initial: import Vincent5588 vault from iCloud"
git branch -M main
git remote add origin git@github.com:Vincent5588/vincent5588-vault.git
git push -u origin main2.3 Gateway sync 腳本
寫到 ~/scripts/vincent5588-gateway-sync.sh,內容見「附錄 A:腳本內容」。
chmod +x ~/scripts/vincent5588-gateway-sync.sh加 alias 方便日常使用:
echo "alias gsync='~/scripts/vincent5588-gateway-sync.sh'" >> ~/.zshrc
source ~/.zshrc之後在任何 terminal 打 gsync 就同步。
2.4 Cloudflare Pages 專案
1. 開 https://dash.cloudflare.com → Workers & Pages
2. Create application → Pages(注意:底部小字「Looking to deploy Pages? Get started」)
3. Direct Upload 模式(不要 Connect to Git,會誤建 Worker)
4. Project name: vincent5588-wiki
5. 拖一個 placeholder index.html 上去先佔位
6. Deploy → URL: https://vincent5588-wiki.pages.dev
2.5 Cloudflare API token
1. https://dash.cloudflare.com/profile/api-tokens
2. Create Token → Custom token → Get started
3. Token name: github-actions-pages-deploy
4. Permissions: Account → Cloudflare Pages → Edit (只要這 1 條)
5. Account Resources: Vincwen@gmail.com's Account
6. Token expiration: No expiration(或 1 year)
7. Create Token → 立刻複製 token 字串
⚠️ Token 字串只顯示 1 次,立刻拿去設 GitHub secret。絕對不要貼 chat / 公開地方。
2.6 GitHub repo secrets
GitHub repo → Settings → Secrets and variables → Actions
加 2 個 secrets:
CLOUDFLARE_API_TOKEN: <你 Cloudflare token>
CLOUDFLARE_ACCOUNT_ID: 7778546cd9e1bfefefd115315a35e888
2.7 GitHub Actions + Quartz 配置
repo 內結構:
.github/
├── workflows/
│ └── deploy.yml ← GitHub Actions 主流程
└── quartz-config/
├── quartz.config.ts ← Quartz 主設定(主題色 / 字型 / plugins)
├── draftWarningBanner.ts ← 客製 plugin(draft/deprecated banner)
└── custom.scss ← 客製 CSS(中文字型 / banner / 暗色微調)
⚠️ 重要踩雷紀錄:原本有 quartz.layout.ts 客製 layout,但每次加都會把整個 3 欄排版打壞。最終決定砍掉,用 Quartz v4.5.2 內建預設 layout(folder 2 欄、content 3 欄)。
設定詳見「附錄 B:完整 deploy.yml」、「附錄 C:quartz.config.ts」。
2.8 第一次 push + build
cd ~/vincent5588-git
mkdir -p .github/workflows .github/quartz-config
# 把 4 個檔案放進去(內容見附錄)
git add .github/
git commit -m "ci: add Quartz build + Cloudflare Pages deploy"
git push origin mainGitHub Actions 自動跑 → 2-4 分鐘 → 部署完成。
3. 日常工作流程
1. 在 Obsidian 編輯 vault(在 iCloud 路徑)
2. 存檔
3. 打開 terminal 跑:
gsync
(或全名 ~/scripts/vincent5588-gateway-sync.sh)
4. 等 1-2 分鐘 GitHub Actions 跑完
5. 重整 https://vincent5588-wiki.pages.dev/ 看到新版
確認同步是否成功
# 查 git log(最新 commit)
cd ~/vincent5588-git && git log --oneline -3
# 查 gateway log
tail ~/vincent5588-git/.gateway.log
# 看 GitHub Actions 跑完了沒
# 開: https://github.com/Vincent5588/vincent5588-vault/actions4. 客製化清單
4.1 已完成的客製
| 項目 | 設定位置 | 效果 |
|---|---|---|
| 紫色主題色 | quartz.config.ts colors | secondary #7c3aed, tertiary #a78bfa |
| Serif 標題字型 | quartz.config.ts typography | h1-h6 用 Noto Serif TC |
| Sans 內文字型 | quartz.config.ts typography | body 用 Noto Sans TC |
| Draft banner(黃) | draftWarningBanner.ts + custom.scss | status: draft 頁面顯示警告 |
| Deprecated banner(紅) | draftWarningBanner.ts + custom.scss | status: deprecated 頁面顯示警告 |
| 暗色模式微調 | custom.scss | 程式碼框 / 引用 / code 配紫色 |
| 強調最後修改日期 | custom.scss | ContentMeta 第一個 span 變粗 |
| 中文 locale | quartz.config.ts | locale: “zh-TW” |
/ redirect | deploy.yml | 自動跳到 /wiki/ |
| 排除 explorer | quartz.config.ts ignorePatterns | 不顯示 quartz-engine / .github / Excalidraw |
4.2 沒做的(v1.1+ 可加)
| 項目 | 為什麼沒做 | 怎麼加 |
|---|---|---|
| 自訂網域 | pages.dev 已夠用 | Cloudflare Pages → Custom domains |
| Cloudflare Access SSO | vincent5588 是個人 vault 公開即可 | 適合 IT vault |
| RecentNotes widget | 需要 layout.ts 但會打壞排版 | 需先 debug layout.ts |
| 字數統計 | Quartz 沒內建 | 寫客製 plugin |
| Comments(giscus) | 個人 vault 不需要 | 一般博客可加 |
| PWA 離線可看 | 沒測過 | 加 manifest.json + sw.js |
4.3 Vault → Quartz 過濾規則(兩段 filter pipeline)
兩段 filter — 改前要清楚是擋哪一段:
iCloud vault ~/vincent5588-git/ quartz-engine/content/ │ │ │ ├─ Stage 1: gsync ────rsync --delete───►│ │ │ (§附錄 A) │ │ │ ├─ Stage 2: deploy.yml ──rsync─► │ │ │ (§附錄 B 第 50-69 行) │ │ │ │ │ │ │ └─ Quartz build → 線上站Stage 1 砍永遠不該入 git 的(敏感 / 暫存 / cache)。Stage 2 砍入 git 但不該公開的(archive / 私人 / build artifact)。
Stage 1:gsync excludes(vault → git repo)
§附錄 A 完整 list;分類整理:
| 類別 | 排哪些 | 為什麼 | 加自 |
|---|---|---|---|
| Obsidian 本機 state | .obsidian/workspace* / .obsidian/cache / .obsidian/snippets | 個人 UI / cache,不該入 git | v1.0 |
| 垃圾 / 衝突 | .trash/ / .DS_Store / *conflict* | 系統檔 / Obsidian 衝突解決暫存 | v1.0 |
| Plugin cache | .smart-env/ / .claudian/ | 含 → ? 等特殊字元 filename,rsync 會 abort | v1.3(§雷 11) |
| 本機敏感 | .claude/ | Claude Code OAuth token —— 絕對不該入 git | v1.3 |
| Skill 開發中 / staging | wiki/_skill-staging/ / wiki/_review-queue/ / wiki/reports/ / wiki/dist/ | 中間產物 / 私下進行中 | v1.0 |
| 隱私 | private/ | 私人不公開 | v1.0 |
| Repo metadata | .github/ / .git/ | git 自身(.github/ 是 v1.2 加,§雷 10) | v1.0 / v1.2 |
規則:Stage 1 任何不該入 git 的都該擋在這層。等到了 git repo 才反悔的東西就會跟著 push 到 GitHub 公開——尤其 OAuth token / API key 這類洩漏一次就慘。
Stage 2:deploy.yml excludes(git repo → Quartz build copy)
§附錄 B 第 50-69 行 Copy vault content to Quartz step。跟 Stage 1 大致相同,額外排:
| 額外排 | 為什麼 |
|---|---|
raw/09-ARCHIVE/ | 已 archive 的舊 raw,沒必要公開且占空間 |
node_modules/ | npm 暫存(git repo 理論不會有但保險) |
規則:Stage 2 用來擋「入 git 但不公開」——譬如某些客戶機密、archive、build artifact。這層擋的東西仍在 git history 內,所以對抗的不是洩漏(已洩漏給 GitHub)而是讓線上站視覺上更乾淨。
排了 = 不上線。 沒排 = 會上線
下面是目前會上線的目錄 / 檔案,附風險評估給你 review:
| 路徑 | 目前狀態 | 風險評估 | 建議行動(你慢慢看再決定) |
|---|---|---|---|
00-Inbox/ | ✅ 會上線 | ⚠️ 含 Daily 個人日誌、半成品 | 考慮 Stage 2 排 00-Inbox/Daily/ 或整個 00-Inbox/ |
10-Notes/ | ✅ 會上線 | 🟢 永久筆記,公開 OK | |
20-Projects/ | ✅ 會上線 | ⚠️ 含進行中專案 source(PAM / CallIT 客戶 / 業務邏輯) | 強烈建議 review——客戶機密敏感的考慮 Stage 2 排,或拆 20-Projects/<X>/private/ 子夾 |
30-Areas/ | ✅ 會上線 | 🟢 長期領域 | |
40-Resources/ | ✅ 會上線 | 🟡 含書籍筆記、外部素材 | 確認版權 / 引用規範 |
50-Archive/ | ✅ 會上線 | 🟡 已 archive 的內容 | 想公開歷史就留,不想就 Stage 2 排 |
Templates/ | ✅ 會上線 | 🟢 模板,公開 OK 但其實沒看頭 | 想隱藏 → Stage 2 排 |
Attachments/ | ✅ 會上線 | 🟡 圖片 / PDF | 大檔會佔 build 時間 / Cloudflare 流量;可選擇 Stage 2 排 |
raw/(除 09-ARCHIVE) | ✅ 會上線 | ⚠️ 原始素材會公開——含 Heptabase / Obsidian export / 文章複本 | 建議 review——某些 raw 子夾(譬如 raw/04-meeting-notes/)可能含內部資訊 |
wiki/(核心 entity / map / index / log / daily) | ✅ 會上線 | 🟢 LLM 編譯版,乾淨 | |
CLAUDE.md / Welcome.md / README.md / Vincent5588_Wiki_部署紀錄.md | ✅ 會上線 | 🟢 規範文件,sanitized | 部署紀錄要持續 sanitize(不要寫實際 token / API 路徑) |
候選排除清單(待 review,照優先序)
- [ ] 🔴 高 — `20-Projects/PAM/sources/`(客戶業務邏輯、未公開規格)
- [ ] 🔴 高 — `20-Projects/CallIT/sources/`(同上)
- [ ] 🟡 中 — `00-Inbox/Daily/`(個人日誌,可能含情緒 / 反思)
- [ ] 🟡 中 — `raw/04-meeting-notes/`(會議紀錄,可能含同事姓名 / 內部專案名稱)
- [ ] 🟡 中 — `raw/05-books/`(書籍 raw,版權考量)
- [ ] 🟢 低 — `Attachments/`(純大小考量 —— 大圖會吃 build 時間 / 流量)
- [ ] 🟢 低 — `Templates/`(沒看頭,純美觀)
- [ ] 🟢 低 — `50-Archive/`(看你想不想保留歷史)修法(兩段哪段加要看你想擋什麼)
| 想擋⋯ | 在哪加 | 效果 |
|---|---|---|
| 含敏感資料 / token / 個資 | Stage 1 (~/scripts/vincent5588-gateway-sync.sh) | 連 GitHub repo 都不會有,最安全 |
| 入 git 沒問題但不想公開 | Stage 2 (~/vincent5588-git/.github/workflows/deploy.yml 的 Copy step) | 線上站看不到,git history 仍有 |
| 想全擋 | 兩段都加 | 最嚴 |
加一條 exclude 的 SOP:
# Stage 1(gsync)— 敏感資料
sed -i.bak "s|--exclude='.git/'|--exclude='X/' \\\\\n --exclude='.git/'|" ~/scripts/vincent5588-gateway-sync.sh
# Stage 2(deploy.yml)— 不公開
# 編輯 ~/vincent5588-git/.github/workflows/deploy.yml 的 rsync block,加 --exclude='X/'
nano ~/vincent5588-git/.github/workflows/deploy.yml
# 改完 commit + push
cd ~/vincent5588-git && git add -A && git commit -m "exclude X from build" && git push加完同步更新 §附錄 A / B —— 規則 F「破壞性 flag 必對應 exclusion list」+「文件即真理」。
5. 踩雷紀錄
雷 1:launchd 跑 gateway script 失敗(exit code 23)
症狀:
rsync(69142): error: open /Users/vincent/Library/Mobile Documents/...: Operation not permitted
原因:macOS launchd 跑的 bash 沒權限讀 iCloud Drive(TCC 安全機制)。
解決方案:
- (a) 給
/bin/bash完整磁碟存取權(系統設定 → 隱私權) - (b) 改成手動跑 + Obsidian 熱鍵 / terminal alias(最終採用)
→ 我們選 b:放棄自動排程,改手動觸發 gsync。
雷 2:Cloudflare 創建時誤建 Worker 不是 Pages
症狀:URL 變 *.workers.dev 不是 *.pages.dev,build log 跑 npx wrangler deploy 報錯「Could not detect static files」。
原因:Cloudflare 新 UI 把 Pages 藏起來了,預設 Create application 走 Workers 流程。
解決方案:
- 在 Workers/Pages 創建頁最下方找小字「Looking to deploy Pages? Get started」
- 點那個連結進 Pages 流程
- 選 Direct Upload(不要 Connect to Git)
雷 3:Quartz Plugin.ExplicitPublish() 過濾掉所有頁
症狀:build 成功,但網站全 404,只有 Quartz 框架沒內容。
原因:ExplicitPublish filter 只發佈 frontmatter publish: true 的頁。我們 vault 沒這欄位 → 全部跳過。
解決方案:從 quartz.config.ts 移除 Plugin.ExplicitPublish()。
雷 4:rsync exit code 24(vanished file)
症狀:build log「file has vanished: .gitkeep」+「exit code 24」。
原因:Quartz 內建的 quartz-engine/content/.gitkeep 在 rsync --delete 時消失了,rsync 視為錯誤。
解決方案:先 rm -rf quartz-engine/content 整個清空,再 rsync 不加 --delete。
雷 5:客製 quartz.layout.ts 把 3 欄排版打壞
症狀:build 成功,但網站變單欄(左 sidebar 跟右 sidebar 都不見了,只剩中間內容直直一條)。
原因:未知。即使我寫的 quartz.layout.ts 跟 Quartz default 幾乎一樣,加了就壞。可能是 Quartz v4.5.2 對某些組件有特殊期待。
解決方案:完全不用客製 layout.ts,讓 Quartz 用內建預設。
→ 結果:folder pages 2 欄(無右 sidebar)、content pages 3 欄(含 graph/toc/backlinks)。可接受的妥協。
雷 6:URL / 顯示 404
症狀:vault 根沒 index.md,所以 / 路徑沒首頁。
解決方案:在 GitHub Actions 加一行寫 _redirects 檔:
- name: Add homepage redirect to /wiki/
run: |
echo "/ /wiki/ 302" > quartz-engine/public/_redirects→ / 自動 302 跳到 /wiki/。
雷 7:Cloudflare API token 不小心貼 chat
症狀:Token 流出(即使對話框可能不被記錄)。
解決方案:立刻 rotate(Cloudflare → API Tokens → 該條 → ⋯ → Roll),然後更新 GitHub secret。
→ 教訓:永遠不要把 token / password 貼到任何聊天框。
雷 8:custom.scss 沒 import base.scss → 整個 layout 不見(最大隱形 bug)
症狀:滿版桌面瀏覽器(>1200px)看 wiki 仍是單欄——左欄 explorer 跑到 content 上方堆疊,右欄 graph/toc/backlinks 整個不見、改成大片空白。CF cache purge、無痕視窗、視窗拉滿都救不回來。
原因:Quartz upstream 在 quartz/styles/ 自己有一份 custom.scss,內容只有兩行:
@use "./base.scss";
// put your custom CSS here!這個 @use "./base.scss" 是 base.scss(含整個 .page > #quartz-body { display: grid; grid-template-areas: ... } 三欄 layout)進入編譯結果的唯一管道。
我們的 deploy.yml「Apply custom Quartz config」步驟做:
cp .github/quartz-config/custom.scss quartz-engine/quartz/styles/custom.scss把 upstream 那份含 @use 的檔案整個蓋掉。我們自己的 custom.scss 第一行就是 banner 樣式註解,沒 import base.scss → base.scss 從未進入編譯 → grid layout 規則 0 條 → 瀏覽器 fallback 成 block flow → 全頁單欄。
鐵證(A/B diff):
| 指標 | Default Quartz welcome | 我們的 deploy |
|---|---|---|
index.css 大小 | 35 KB | 17 KB(少一半) |
#quartz-body { display: grid } 規則 | ✅ 3 條(desktop/tablet/mobile) | ❌ 0 條 |
.page > #quartz-body grid template | ✅ 完整 areas | ❌ 沒有 |
解決方案:.github/quartz-config/custom.scss 第一行加 @use "./base.scss";:
@use "./base.scss"; // ⚠️ 必保留,拿掉 = layout 全死,整頁變單欄
// 以下放 banner / 暗色微調等與 layout 無關的樣式
.draft-warning { ... }→ 加完重 build → CSS 大小回到 ~35 KB → 3 欄 layout 回來。
教訓:
蓋掉 upstream 同名檔之前,先讀 upstream 那份在做什麼。
custom.scss 不是「給你的空檔案」,是 upstream 留的入口檔,帶有必要的 @use 鏈結。cp 蓋同名檔會把 upstream 的鏈結邏輯一起殺掉。
這個 bug 從第一次 push 部署就在了。banner 樣式仍在運作(看起來像「只是 layout 出問題」),所以沒人懷疑整個 base.scss 從沒編譯。錯了多次重 build 都沒抓到,最後是用 curl 抓 Quartz upstream custom.scss 對 + diff deployed CSS vs default Quartz welcome CSS 才確認。
→ 對 IT Wiki 部署的啟示:任何 cp 同名檔之前,先 curl https://raw.githubusercontent.com/.../tagref/path 看 upstream 內容。
雷 9:deploy.yml 飄離部署紀錄(文件腐化 / doc-vs-code drift)
症狀:build 失敗 / 行為不對,但部署紀錄寫的解法明明就是對的。
原因:部署紀錄是「該怎麼做」的真理錨點,但實際 .github/workflows/deploy.yml 在後續編輯時沒對著紀錄校對就被改了,行為跟紀錄不一致。一天內踩到 3 次:
| # | 部署紀錄寫的 | 實際 deploy.yml 跑的 | 後果 |
|---|---|---|---|
| 1 | git clone --branch v4.5.2 ...quartz.git 鎖版本 | git clone --depth=1 ...quartz.git(沒鎖) | 每次 build 抓 Quartz HEAD,同 commit 不同結果 |
| 2 | §雷 4:rm -rf content + rsync 不加 --delete | mkdir -p content + rsync -a --delete | upstream .gitkeep 在 transfer 中 vanish → exit 24 build 失敗 |
| 3 | §雷 6:redirect 跳 /wiki/(trailing slash) | redirect 跳 /wiki/index | 根目錄 302 → 404(/wiki/index 路由不存在) |
解決方案(短期):三項全部修回紀錄版本,並在 deploy.yml 每段加 inline # NOTE 註解指向部署紀錄章節:
# NOTE: must rm -rf first AND not use --delete.
# See deployment record 雷 4.
rm -rf quartz-engine/content
mkdir -p quartz-engine/content
rsync -a \
...解決方案(長期):每季對著本部署紀錄跑一次 deploy.yml diff,找出飄離段落並修回(或更新紀錄反映新事實)。CLAUDE.md §10 加經驗法則 3 把這條規則化。
教訓:
「文件就是真理」這句話只在文件被持續校對時才成立。
紀錄寫得再清楚,下游 code 沒有指向 doc 的 inline reference,就會在無人注意時飄走。每個重要 yaml / script 段落都要有 # See deployment record §X 之類的註腳——讓編輯者改之前先看 doc,而不是等踩雷才回頭翻。
這是 meta-bug:紀錄正確 ≠ 系統正確,紀錄與實作之間需要雙向錨。
→ 對 IT Wiki 部署的啟示:deploy.yml 第一次寫好之後,每段重要 step(rsync、cp、build 設定)都加 # See <doc-section> 註腳。每季排一個 calendar reminder「diff deploy.yml vs 部署紀錄」。
雷 10:gsync rsync --delete 沒排除 .github/ → workflow 跟 quartz-config 全被砍(day-one 隱形 bug)
症狀:跑完 gsync 後 https://github.com/Vincent5588/vincent5588-vault/actions 沒任何新 workflow run 觸發。明明 commit + push 都成功了。
[2026-05-03 17:27:42] scanning sensitive content...
To github.com:Vincent5588/vincent5588-vault.git
9f59d4e..3e1556e main -> main
[2026-05-03 17:27:45] pushed: auto: 2026-05-03 17:27
[2026-05-03 17:27:45] === Gateway sync done ===
→ 但 GitHub Actions tab 死寂無聲。
逐層往前查的過程:
- ❓ 該不會 deploy.yml 在 main 不在?→ 開 GitHub repo
.github/workflows/路徑,整個目錄不存在 - ❓ 那本機
~/vincent5588-git/.github/workflows/呢?→$ ls ~/vincent5588-git/.github/workflows/ ls: ...: No such file or directory - ❓
.github/這個 dir 整個沒了?→$ ls ~/vincent5588-git/.github/ ls: ...: No such file or directory - ❓ vault 裡有嗎?→ vault 從來沒有
.github/目錄(這個是 Quartz/CI 配置,不該放 Obsidian 裡) - ❓ gsync rsync 設了什麼 exclude?→ 看 §附錄 A:
--exclude='.git/' \ ← 只排除 .git/.github/不在 excludes 裡
根本原因:.git/ 跟 .github/ 是兩個不同目錄。rsync --exclude='.git/' 不 match .github/。所以 gsync 跑 rsync -a --delete 從 vault 同步到 ~/vincent5588-git/ 時:
- vault 沒有
.github/ --deleteflag 會「同步到目標:source 沒有的就刪」- → 每次 gsync 都砍掉本機
.github/整棵樹(含 workflows/deploy.yml + quartz-config/{quartz.config.ts, draftWarningBanner.ts, custom.scss}) - → push 後 GitHub repo 也沒
.github/ - → GitHub Actions 找不到 workflow 檔,靜默不觸發(不會報錯,沒人會發現)
為什麼 v1.0/v1.1 寫的時候沒抓到:
(a) Phase 0–9 一次性 setup 是手動建檔——把 deploy.yml 直接在 ~/vincent5588-git/ 建好,那時候還沒跑過 gsync,所以 push 後 Action 是有跑的(v1.0 §2.8 §3 都驗證過 build 成功)。
(b) 第一次 gsync 跑了之後 .github/ 就被砍了——但因為當下沒馬上 push(或 push 後沒立刻檢查 Actions),沒注意到。後續 vault 改動如果是只改 wiki/ 內容,gsync push 後 GitHub 還是收到 commit,但 Actions tab 永遠不更新。
(c) gsync log 顯示 push 成功就以為 deploy 也成功——push 成功 ≠ Actions 觸發。沒在 log 加 Actions tab check。
(d) 這是經驗法則 3「文件即真理失效要回查 source code」的教科書範例:
- 部署紀錄 §3 寫「打 gsync 後 1-2 分鐘 Cloudflare 拉新版」——正確答案
- 實際行為:gsync 砍
.github/→ push → Actions 不跑 → Cloudflare 不拉 - 兩邊 drift 久了沒人發現,因為「沒看到網站更新」很容易被歸因為「我今天沒改 vault 內容」
短期還原(已執行):
outputs/restore_github.sh 一鍵跑完三件事:
- 把 §附錄 B/C/D/E 抽出的 4 個檔複製到
~/vincent5588-git/.github/(workflows/deploy.yml + quartz-config/* 3 個) - 修
~/scripts/vincent5588-gateway-sync.sh加--exclude='.github/'(在--exclude='.git/'上方) cd ~/vincent5588-git && git add .github/ && git commit -m "fix: restore .github/ wiped by gsync rsync --delete (§雷 10)" && git push
長期防呆:
(a) §附錄 A 同步加 --exclude='.github/'(v1.2 修)+ 加 # NOTE 指向本雷
(b) gsync 跑完應該補一個 step:curl -s -o /dev/null -w "%{http_code}\n" https://api.github.com/repos/Vincent5588/vincent5588-vault/actions/runs?per_page=1 看最新 run timestamp 跟 commit timestamp 的 diff(>5 分鐘 = Actions 沒跑,警告)
(c) 每月做一次手動驗證:vault 改一個 trivial 改動 → gsync → 5 分鐘後看網站有沒有更新
跟其他雷的關係:
| 雷 # | 跟雷 10 的關聯 |
|---|---|
| 雷 1(launchd 失敗 → 改手動 gsync) | 觸發了 gsync 這個工具的存在 |
雷 4(rsync --delete 砍 .gitkeep) | 同樣是 --delete 引起的雷,但雷 4 修的是 build pipeline 內的 rsync,不是 gsync |
雷 8(custom.scss 沒 @use \"./base.scss\") | 兩雷一起發生時加倍致命:custom.scss 被 gsync 砍 → 重建沒帶 @use → layout 死 |
| 雷 9(doc-vs-code drift) | §附錄 A 跟實際 gsync 一致沒 drift——但部署紀錄正文跟 gsync 的行為有 drift(§3 寫「Action 跑」,實際「Action 不跑」)。屬於 doc-vs-behavior drift |
經驗法則進化:
→ 寫進 vault CLAUDE §10 經驗法則 3 補充「文件即真理失效」的偵測信號清單,加一條:
- **「應該觸發某 side effect 但沒觸發」**也是 drift 信號(不只是「行為錯」)。Quiet failure = silent drift。
→ 規則 F「deploy pipeline 抓上游 repo 必鎖 tag/sha」可以擴成更廣的「任何破壞性 flag(--delete、--force、rm -rf 等)都必須對應一份 exclusion list 並逐字 review」。雷 10 就是 --delete 沒對應完整 exclusion list 的後果。
雷 11:rsync 撞 Obsidian plugin cache 內特殊字元 filename → “Illegal byte sequence”
症狀:修完 §雷 10、剛把 .github/ 加到 excludes 後第一次重跑 gsync,立刻死在 rsync:
rsync(4029): error: ...卡片盒筆記法_..._用卡片盒筆記法,建立知識連結網路來活用筆記_→_國外研究...專家與粉絲,給予新手幾個使用原則?.619bsejpkV': Illegal byte sequence
rsync(4028): error: ...md.ajson: utimensat (2): No such file or directory
rsync(4028): error: unexpected end of file
=== Gateway sync done === ← 但 exit code 23
❌ 同步失敗(exit code ??)
原因:vault 根有 .smart-env/ 目錄——是 Smart Connections plugin 的本機 embedding cache。它把每篇筆記產一個 .ajson 暫存檔,filename 直接把標題 substitute 成 path-safe——但 substitute 不夠徹底:
→跟?(全形問號)都被當合法 filename 字元留下- macOS APFS 接受這些(HFS+/APFS 是 NFC + Unicode aware)
- 但 rsync 在 byte-level transfer 時碰到非 UTF-8 sequence(檔名是 NFD precomposed + 特殊 Unicode)就 abort
.ajson是 partial / temp 檔,rsync 看到後 source 已刪 →utimensat: No such file
為什麼 .smart-env/ 沒在 §雷 10 修補時一起加進 excludes:
(a) §雷 10 修補時手上沒這 trace ——只看到 .github/ 被砍,沒人想到 vault 裡還有其他第三方 plugin 在亂寫東西
(b) §附錄 A 寫 v1.0 時 .smart-env/ 還不存在(Smart Connections 是後來才裝的)
(c) 這就是規則 F 廣義版 (b) 條的反例:「每加一個新類型的檔到目標位置,必同步更新 exclude list」——使用者裝 Smart Connections 時沒同步 update gsync。
修法:
outputs/Fix gsync excludes.command 一鍵跑:
- 備份原 gsync 到
.bak.{timestamp} - 用 awk 在
--exclude='.git/'上方插入 3 行:--exclude='.smart-env/'(Smart Connections cache)--exclude='.claudian/'(Claudian plugin cache,預防性加入)--exclude='.claude/'(Claude Code 本機設定 + OAuth token,敏感)
- chmod +x 保持可執行
- 立刻重跑 gsync 驗證
長期防呆:
(a) §附錄 A 同步加新 excludes + # NOTE 分組註解(4 群:.github / plugin caches / 敏感設定 / .git 順序),這樣未來看到 § 附錄 A 的人能理解「為什麼這 4 群必須各自獨立」
(b) 寫進規則 F 廣義版的 day-one 防呆 (b):裝新 Obsidian plugin 時 SOP 應該包含「檢查它是不是會在 vault 寫 cache,如有 → 加進 gsync excludes」
(c) 月度維運提示(§7.1)加一條:「掃 vault 根有沒有新的隱藏目錄(.smart-env / .claudian / .X),每個都該決定要不要排」
預防性加的 .claudian/ / .claude/:
這次只有 .smart-env/ 真的撞錯,但同樣是 plugin cache 性質的 .claudian/ 也應該排(沒踩雷只是因為 Claudian 的 cache filename 比較規矩)。.claude/ 是 Claude Code 本機設定含 OAuth token,絕對不該入 git,應該老早就 exclude——v1.0 漏列是另一個 §雷 9 doc-vs-actual drift 候選。
跟其他雷的關係:
- 雷 9:drift 概念
- 雷 10:
--delete沒 exclusion list 直接案例 - 雷 11:不是
--delete砍掉,是rsync本身遇 illegal byte abort——但本質還是 exclusion list 不完整
雷 12:Quartz v4.5.2 不支援 Obsidian Dataview runtime → 70+ block 線上站全變 raw code block
症狀:vault 內所有 dataview ... block 在線上站(vincent5588-wiki.pages.dev)顯示為原始 query code,沒被執行成 table。
原因:Obsidian Dataview 是 runtime 執行的 JS 查詢引擎(每次開頁時 query vault metadata 即時算)。Quartz 是 build-time 靜態 site generator——它把 markdown 編譯成 HTML 後就完事了,沒辦法在使用者瀏覽時 query 任何東西。所以 ```dataview block 對 Quartz 來說只是普通 code block。
修法(v1.4 採用):build 時用 Python script 把 ```dataview block 預編譯成靜態 markdown table:
# deploy.yml 加 step(見 §附錄 B 第 71-78 行)
- name: Convert Dataview blocks to static markdown
run: |
pip install pyyaml --quiet
python3 .github/scripts/dataview_to_md.py --root quartz-engine/content/跑在 Copy vault content to Quartz 之後、Build Quartz site 之前——只動 build copy 不動 source vault。
支援取捨(converter 設計):
| Query 型態 | 處理 | 比例 |
|---|---|---|
TABLE [WITHOUT ID] field AS "col", ... FROM "path" [WHERE ...] [SORT ...] [LIMIT n] | ✅ 編譯成 markdown table | ~46/72 (64%) |
LIST [field] FROM "path" [WHERE ...] [SORT ...] [LIMIT n] | ✅ 編譯成 bullet list | 同上 |
GROUP BY / FLATTEN / DataviewJS / TASK / CALENDAR | ⚠️ 留靜態 placeholder(提示「請開 Obsidian 看實時」) | ~26/72 (36%) |
為什麼不全部支援:
(a) GROUP BY 涉及 aggregation,要寫一套 fake-SQL engine (b) FLATTEN 涉及 array 展開 (c) DataviewJS 是任意 JS 程式碼,要整套 V8 sandbox (d) 完整 DQL parser 是多日工程;簡單 80% case 一個 ~400 行 Python 解決——成本效益極好
→ 不支援的少數 query 留 placeholder(> ℹ️ Dataview Query(Quartz 不支援 GROUP BY ... 此處顯示原 query;實時結果請開 Obsidian))。
支援的 field expression:
file.link→[[basename]](Quartz 會 render 成 internal link)file.name/file.path/file.ctime/file.mtime/file.sizefile.tags→ comma-separated stringfile.inlinks/file.outlinks→ 透過 vault scan 算length(file.inlinks)/length(file.outlinks)→ 數值- 任何 frontmatter 欄位(
type/status/domain/updated/created/tags/ …)
支援的 WHERE:
field = "value"/field != "value"field = field(field-to-field 比較,少見)length(file.inlinks) = 0/length(file.X) > N<expr> AND <expr>(chain)- ❌ OR / contains / nested 不支援(會 fallback 到「全 match true」保守處理)
長期考量:
(a) v1.4 採 build-time approach:每次 push 都重建一份最新 snapshot,不會 stale (b) 如果 query 數量繼續長 / 複雜化,可考慮升級到 Quartz Custom Plugin 寫成 Quartz transformer plugin (c) 不會用 Quartz 第三方 dataview plugin(搜過社群——多半沒維護或只支援部分)
驗證:
build 完到 https://vincent5588-wiki.pages.dev/wiki/maps/Wiki-儀表板 看 13 個 dataview block 是否都變 table。如果某個沒變或顯示空 table,可能是:
- DQL 用了 GROUP BY → 預期行為,placeholder 顯示
- WHERE 用了 OR / contains → converter 沒解,fallback all-match 但 SORT 仍 OK
- frontmatter 欄位拼錯 → 空 cell(query 對 missing field 回傳空字串)
6. 重要檔案一覽
| 檔案 | 位置 | 用途 |
|---|---|---|
| Vault | /Users/vincent/Library/Mobile Documents/iCloud~md~obsidian/Documents/Vincent5588/ | 編輯 source |
| 本機 git repo | ~/vincent5588-git/ | rsync 過濾後 + git history |
| Gateway 腳本 | ~/scripts/vincent5588-gateway-sync.sh | 手動跑同步 |
| Gateway log | ~/vincent5588-git/.gateway.log | 查看同步歷史 |
| GitHub repo | https://github.com/Vincent5588/vincent5588-vault | 公開(其實是 private) |
| GitHub secrets | repo → Settings → Secrets | CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID |
| Workflows | .github/workflows/deploy.yml | GitHub Actions build pipeline |
| Quartz 設定 | .github/quartz-config/quartz.config.ts | 主題 / plugins / 字型 |
| Banner plugin | .github/quartz-config/draftWarningBanner.ts | draft / deprecated 警告 |
| Custom CSS | .github/quartz-config/custom.scss | 客製樣式 |
| Cloudflare Pages | https://dash.cloudflare.com → Workers & Pages → vincent5588-wiki | Hosting |
| API tokens | https://dash.cloudflare.com/profile/api-tokens | 部署用 token |
| 線上網站 | https://vincent5588-wiki.pages.dev | 公開讀取 |
7. 維運提醒
7.1 每月做一次
- Cloudflare Access logs(看訪問狀況)— 雖然是公開站
- GitHub Actions 跑無異常
-
.gateway.log沒敏感詞警告 - vault
wiki-lint跑一次(確保健康度)
7.2 每季做一次
- Cloudflare API token rotate
- Quartz 升級檢查(半年一次也行)
- vault CLAUDE.md 規範回顧
7.3 緊急回滾
如果某次 push 把網站搞壞:
cd ~/vincent5588-git
git revert HEAD --no-edit
git push→ 1-2 分鐘 GitHub Actions 重 build → 網站還原。
7.4 暫停發佈(譬如旅遊期間不希望意外更新)
不必特別操作——你只要不跑 gsync,網站就停在當前版本。Drive Desktop 同步 iCloud → 你 Mac 是自動的,但 git push 是你手動跑。
8. 給後來想做同樣部署的人的建議
按照這順序最不會踩雷:
Phase 0:先確認你會 git 基本(commit / push / log)
Phase 1:建 GitHub private repo + 本機 git clone
Phase 2:寫 gateway sync 腳本 + 試跑
Phase 3:建 Cloudflare Pages(注意走 Direct Upload,不是 Worker)
Phase 4:建 Cloudflare API token(只給 Pages: Edit 權限)
Phase 5:設 GitHub secrets
Phase 6:寫 deploy.yml + Quartz 配置 + push
Phase 7:第一次 build 看結果,逐個調試
Phase 8:客製主題色 / 字型 / banner
Phase 9:(可選)自訂網域 / Cloudflare Access SSO
每階段完成都驗證,有問題立刻 debug 不要往下走。
附錄 A:vincent5588-gateway-sync.sh
#!/bin/bash
# Vincent5588 Gateway: iCloud -> Git auto sync
set -e
VAULT_PATH="$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents/Vincent5588"
GIT_PATH="$HOME/vincent5588-git"
LOG="$GIT_PATH/.gateway.log"
log() {
echo "[$(TZ='Asia/Taipei' date '+%F %T')] $*" | tee -a "$LOG"
}
log "=== Gateway sync start ==="
# 1. 確認 vault 路徑存在
if [ ! -d "$VAULT_PATH" ]; then
log "ERROR: Vault folder missing"
exit 1
fi
# 2. rsync 過濾
log "rsync filtering..."
# NOTE: 三組 excludes 各有專門用途——任何一組漏掉都會出事:
#
# (1) .github/ — vault 內 *永不* 該有 .github/(那是 GitHub Actions workflow +
# Quartz config,由本機 git repo 端維護)。rsync --delete 沒排除
# → 每次 gsync 都會砍 ~/vincent5588-git/.github/ 整棵樹,
# GitHub Actions 靜默失效。詳見 §雷 10。
# (2) .smart-env/ / .claudian/ — Obsidian 第三方 plugin cache,內含 `→` `?` 等
# 特殊字元 filename 跟 .ajson 暫存檔。rsync 撞 encoding 出錯
# → "Illegal byte sequence" 跟 "utimensat: No such file",
# 整個同步 abort。詳見 §雷 11。
# (3) .claude/ — Claude Code 本機設定(含 OAuth token 等敏感資料)+ 不該入 git。
# (4) .git/ — 已有但**位置必須在所有其他 .git* 系列 exclude 後面**,因為
# rsync exclude 是字串前綴 match,順序敏感。
rsync -a --delete \
--exclude='.obsidian/workspace*' \
--exclude='.obsidian/cache' \
--exclude='.obsidian/snippets' \
--exclude='.trash/' \
--exclude='.DS_Store' \
--exclude='*conflict*' \
--exclude='wiki/_skill-staging/' \
--exclude='wiki/_review-queue/' \
--exclude='wiki/reports/' \
--exclude='wiki/dist/' \
--exclude='private/' \
--exclude='.github/' \
--exclude='.smart-env/' \
--exclude='.claudian/' \
--exclude='.claude/' \
--exclude='.git/' \
"$VAULT_PATH/" "$GIT_PATH/"
# 3. 偵測敏感詞
log "scanning sensitive content..."
SENSITIVE=$(grep -rE -l "(password|api[_-]?key|secret[_-]?key|access[_-]?token)[[:space:]]*[:=][[:space:]]*['\"]?[A-Za-z0-9]{10,}" "$GIT_PATH" --exclude-dir=.git 2>/dev/null || true)
if [ -n "$SENSITIVE" ]; then
log "WARNING: Possible sensitive content (review later):"
echo "$SENSITIVE" | head -5 | tee -a "$LOG"
fi
# 4. git commit + push
cd "$GIT_PATH"
if git diff --quiet && git diff --cached --quiet; then
log "no changes, skip"
else
git add -A
COMMIT_MSG="auto: $(TZ='Asia/Taipei' date '+%F %H:%M')"
git commit -m "$COMMIT_MSG" >/dev/null
if git push origin main 2>&1 | tee -a "$LOG"; then
log "pushed: $COMMIT_MSG"
else
log "push failed (will retry next cron)"
exit 1
fi
fi
log "=== Gateway sync done ==="
echo "" >> "$LOG"附錄 B:deploy.yml 完整內容
同步原則:本附錄必須跟
~/vincent5588-git/.github/workflows/deploy.yml逐字一致。 若實際檔有改動,立刻同步本附錄;反之亦然。每段重要 step 帶# NOTE指向本紀錄章節, 修檔者改之前必須回頭看 doc。詳見 §雷 9。
name: Build and Deploy to Cloudflare Pages
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- name: Checkout vault
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Clone Quartz engine
run: |
# NOTE: 必鎖 --branch v4.5.2。沒鎖 → 每次 build 抓 Quartz HEAD,
# 同 commit 不同結果(曾踩到,見 §雷 9 #1)。
git clone --depth=1 --branch v4.5.2 https://github.com/jackyzha0/quartz.git quartz-engine
- name: Install Quartz dependencies
working-directory: quartz-engine
run: npm install --legacy-peer-deps
- name: Apply custom Quartz config
run: |
cp .github/quartz-config/quartz.config.ts quartz-engine/quartz.config.ts
# 不覆蓋 quartz.layout.ts,使用 Quartz 內建預設(避免 layout 被打壞,§雷 5)
cp .github/quartz-config/draftWarningBanner.ts quartz-engine/quartz/plugins/transformers/
mkdir -p quartz-engine/quartz/styles
# NOTE: custom.scss 第一行必有 `@use "./base.scss";` — 否則 layout 全死(§雷 8)
cp .github/quartz-config/custom.scss quartz-engine/quartz/styles/custom.scss
- name: Copy vault content to Quartz
run: |
# NOTE: must rm -rf first AND not use --delete.
# Quartz upstream ships quartz-engine/content/.gitkeep; with --delete
# rsync schedules it for removal mid-transfer and exits 24 ("file
# vanished"). Wipe the dir clean instead. See §雷 4 + §雷 9 #2.
rm -rf quartz-engine/content
mkdir -p quartz-engine/content
rsync -a \
--exclude='.git/' \
--exclude='.github/' \
--exclude='.obsidian/workspace*' \
--exclude='.obsidian/cache' \
--exclude='.obsidian/snippets' \
--exclude='.trash/' \
--exclude='.DS_Store' \
--exclude='*conflict*' \
--exclude='wiki/_skill-staging/' \
--exclude='wiki/_review-queue/' \
--exclude='wiki/reports/' \
--exclude='wiki/dist/' \
--exclude='raw/09-ARCHIVE/' \
--exclude='private/' \
--exclude='node_modules/' \
./ quartz-engine/content/
- name: Register custom plugin in config
working-directory: quartz-engine
run: |
if ! grep -q "DraftWarningBanner" quartz/plugins/transformers/index.ts; then
echo "export { DraftWarningBanner } from \"./draftWarningBanner\"" >> quartz/plugins/transformers/index.ts
fi
- name: Build Quartz site
working-directory: quartz-engine
run: npx quartz build
- name: Add homepage redirect to /wiki/
run: |
# Quartz routes wiki/index.md to /wiki/ (folder index URL,
# trailing slash). NOT /wiki/index — that path does not exist
# and returns 404. See §雷 6 + §雷 9 #3.
echo "/ /wiki/ 302" > quartz-engine/public/_redirects
cat quartz-engine/public/_redirects
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: vincent5588-wiki
directory: quartz-engine/public
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
branch: main附錄 C:quartz.config.ts 完整內容
import { QuartzConfig } from "./quartz/cfg"
import * as Plugin from "./quartz/plugins"
const config: QuartzConfig = {
configuration: {
pageTitle: "Vincent5588 Wiki",
pageTitleSuffix: "",
enableSPA: true,
enablePopovers: true,
analytics: null,
locale: "zh-TW",
baseUrl: "vincent5588-wiki.pages.dev",
ignorePatterns: [
"private", ".obsidian", "Templates", ".trash",
"raw/09-ARCHIVE",
"wiki/_skill-staging", "wiki/_review-queue",
"wiki/reports", "wiki/dist",
"**/.DS_Store", "**/*conflict*",
"node_modules", "quartz-engine", "Excalidraw", ".github",
],
defaultDateType: "modified",
generateSocialImages: false,
theme: {
fontOrigin: "googleFonts",
cdnCaching: true,
typography: {
header: "Noto Serif TC",
body: "Noto Sans TC",
code: "JetBrains Mono",
},
colors: {
lightMode: {
light: "#faf8f8", lightgray: "#e5e5e5",
gray: "#b8b8b8", darkgray: "#4e4e4e", dark: "#2b2b2b",
secondary: "#7c3aed",
tertiary: "#a78bfa",
highlight: "rgba(167, 139, 250, 0.15)",
textHighlight: "#a78bfa44",
},
darkMode: {
light: "#161618", lightgray: "#2a2a2d",
gray: "#7d7d7d", darkgray: "#d4d4d4", dark: "#ebebec",
secondary: "#a78bfa",
tertiary: "#c4b5fd",
highlight: "rgba(167, 139, 250, 0.15)",
textHighlight: "#a78bfa44",
},
},
},
},
plugins: {
transformers: [
Plugin.FrontMatter(),
Plugin.CreatedModifiedDate({ priority: ["frontmatter", "git", "filesystem"] }),
Plugin.SyntaxHighlighting({ theme: { light: "github-light", dark: "github-dark" } }),
Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }),
Plugin.GitHubFlavoredMarkdown(),
Plugin.TableOfContents(),
Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
Plugin.Description(),
Plugin.Latex({ renderEngine: "katex" }),
],
filters: [], // ⚠️ 不要加 ExplicitPublish — 會 filter 掉沒有 publish: true 的所有頁
emitters: [
Plugin.AliasRedirects(),
Plugin.ComponentResources(),
Plugin.ContentPage(),
Plugin.FolderPage(),
Plugin.TagPage(),
Plugin.ContentIndex({ enableSiteMap: true, enableRSS: true }),
Plugin.Assets(),
Plugin.Static(),
Plugin.NotFoundPage(),
],
},
}
export default config附錄 D:draftWarningBanner.ts
import { QuartzTransformerPlugin } from "../types"
export const DraftWarningBanner: QuartzTransformerPlugin = () => ({
name: "DraftWarningBanner",
htmlPlugins() {
return [() => (tree, file) => {
const status = file.data.frontmatter?.status as string | undefined
if (status === "draft") {
;(tree as any).children.unshift({
type: "element",
tagName: "div",
properties: { className: ["draft-warning"] },
children: [
{
type: "element",
tagName: "strong",
properties: {},
children: [{ type: "text", value: "⚠️ Draft(草稿,未驗證)" }],
},
{
type: "text",
value: " 本頁狀態為 draft,內容尚未經過驗證或業務確認。請以 status=stable 的頁面為準。",
},
],
})
}
if (status === "deprecated") {
;(tree as any).children.unshift({
type: "element",
tagName: "div",
properties: { className: ["deprecated-warning"] },
children: [
{
type: "element",
tagName: "strong",
properties: {},
children: [{ type: "text", value: "🔴 Deprecated(已過時)" }],
},
{
type: "text",
value: " 本頁已 deprecated(內容過時 / 已被替代)。請勿用作當前參考。",
},
],
})
}
}]
},
})附錄 E:custom.scss
⚠️ 第一行必須是
@use "./base.scss";——這是 Quartz layout grid 進入編譯結果的唯一管道。拿掉 = 整頁變單欄(見 §雷 8)。
// =============================================================
// CRITICAL: import Quartz base styles (layout grid, default rules)
// 這行替我們把 Quartz 整個 base.scss(含 .page > #quartz-body 的 3-column
// grid layout)接進編譯結果。upstream 的 custom.scss 第一行就是這個。
// 我們這個檔會在 build 時 cp 蓋過 upstream 那份,所以一定要保留這個 @use
// — 拿掉 = 整個 layout 不見、變單欄。歷史教訓見部署紀錄 §雷 8。
// =============================================================
@use "./base.scss";
// Vincent5588 Wiki — Quartz custom styles
// 以下只放 banner / 暗色微調等與 layout 無關的樣式。
// Layout 用 Quartz v4.5.2 內建預設(content 頁 3 欄、folder 頁 2 欄),
// 不要在這裡覆寫 .page / .sidebar / .center 的 grid。歷史教訓 §雷 5。
// =============================================================
// Banner 樣式
// =============================================================
.draft-warning {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 12px 16px;
margin: 16px 0 24px 0;
border-radius: 4px;
color: #856404;
font-size: 0.95em;
line-height: 1.55;
strong {
color: #664d03;
margin-right: 4px;
}
}
.deprecated-warning {
background: #f8d7da;
border-left: 4px solid #dc3545;
padding: 12px 16px;
margin: 16px 0 24px 0;
border-radius: 4px;
color: #721c24;
font-size: 0.95em;
line-height: 1.55;
strong {
color: #58151c;
margin-right: 4px;
}
}
:root[saved-theme="dark"] {
.draft-warning {
background: rgba(255, 193, 7, 0.12);
color: #ffd54f;
strong { color: #ffeb3b; }
}
.deprecated-warning {
background: rgba(220, 53, 69, 0.12);
color: #ff8a80;
strong { color: #ff5252; }
}
}不在這個檔做的事(過去踩過雷):
- 不要覆寫
.page/.sidebar/.center的 grid 規則(selectors 對不上 v4.5.2 DOM,加了會把 default grid 打壞變單欄。歷史教訓 §雷 5)- 不要拿掉
@use "./base.scss";(layout 全死,§雷 8)- 不要改檔名(deploy.yml 第 39 行 cp 到
quartz-engine/quartz/styles/custom.scss是這個名字)
9. 結語
從 0 到網站上線,總時長約 4-5 小時(含踩雷時間)。
關鍵成就:
- ✅ 0 預算(除了你的時間)
- ✅ 0 server 維運(Cloudflare 全自動)
- ✅ 0 寫手學習成本(你繼續用 Obsidian + iCloud)
- ✅ 全網可達(含手機)
- ✅ 全文搜尋 + Mermaid + Backlinks + Graph view 內建
最大教訓:
“Quartz 預設已經很合理,不要過度客製。每次想客製 layout.ts 都會打壞 3 欄排版。”
下一步可能性:
- IT vault 套用同樣 pattern + 加 Cloudflare Access SSO
- 自訂網域
wiki.your-domain.com - 在 vault CLAUDE.md 補強這次踩雷的規則
← 回 Vincent5588 主目錄