Vincent5588 Wiki 公開站部署紀錄

部署日期:2026-05-03 線上網址https://vincent5588-wiki.pages.dev GitHub repohttps://github.com/Vincent5588/vincent5588-vault 核心架構:iCloud vault → Gateway → GitHub → Quartz build → Cloudflare Pages


0. 版本歷程

版本日期主要變動
v1.02026-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.12026-05-03layout 修復事件後續,當天連續踩 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.22026-05-03gsync 砍 .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.32026-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 個正面案例

同步紀律:本檔每次有意義改動(新增章節 / 改 §附錄 / 修步驟 / 加雷 X)都必須在本表加一行 row,跟 [[CLAUDE]] §0 版本歷程同模式。frontmatter updated: 是粗粒度信號,本表才是細粒度差異紀錄。詳見 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 main

2.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 main

GitHub 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/actions

4. 客製化清單

4.1 已完成的客製

項目設定位置效果
紫色主題色quartz.config.ts colorssecondary #7c3aed, tertiary #a78bfa
Serif 標題字型quartz.config.ts typographyh1-h6 用 Noto Serif TC
Sans 內文字型quartz.config.ts typographybody 用 Noto Sans TC
Draft banner(黃)draftWarningBanner.ts + custom.scssstatus: draft 頁面顯示警告
Deprecated banner(紅)draftWarningBanner.ts + custom.scssstatus: deprecated 頁面顯示警告
暗色模式微調custom.scss程式碼框 / 引用 / code 配紫色
強調最後修改日期custom.scssContentMeta 第一個 span 變粗
中文 localequartz.config.tslocale: “zh-TW”
/ redirectdeploy.yml自動跳到 /wiki/
排除 explorerquartz.config.ts ignorePatterns不顯示 quartz-engine / .github / Excalidraw

4.2 沒做的(v1.1+ 可加)

項目為什麼沒做怎麼加
自訂網域pages.dev 已夠用Cloudflare Pages → Custom domains
Cloudflare Access SSOvincent5588 是個人 vault 公開即可適合 IT vault
RecentNotes widget需要 layout.ts 但會打壞排版需先 debug layout.ts
字數統計Quartz 沒內建寫客製 plugin
Comments(giscus)個人 vault 不需要一般博客可加
PWA 離線可看沒測過加 manifest.json + sw.js

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 KB17 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 跑的後果
1git clone --branch v4.5.2 ...quartz.git 鎖版本git clone --depth=1 ...quartz.git(沒鎖)每次 build 抓 Quartz HEAD,同 commit 不同結果
2§雷 4:rm -rf content + rsync 不加 --deletemkdir -p content + rsync -a --deleteupstream .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)

症狀:跑完 gsynchttps://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 死寂無聲。

逐層往前查的過程

  1. ❓ 該不會 deploy.yml 在 main 不在?→ 開 GitHub repo .github/workflows/ 路徑,整個目錄不存在
  2. ❓ 那本機 ~/vincent5588-git/.github/workflows/ 呢?→
    $ ls ~/vincent5588-git/.github/workflows/
    ls: ...: No such file or directory
    
  3. .github/ 這個 dir 整個沒了?→
    $ ls ~/vincent5588-git/.github/
    ls: ...: No such file or directory
    
  4. ❓ vault 裡有嗎?→ vault 從來沒有 .github/ 目錄(這個是 Quartz/CI 配置,不該放 Obsidian 裡)
  5. ❓ 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/
  • --delete flag 會「同步到目標: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 一鍵跑完三件事:

  1. 把 §附錄 B/C/D/E 抽出的 4 個檔複製到 ~/vincent5588-git/.github/(workflows/deploy.yml + quartz-config/* 3 個)
  2. ~/scripts/vincent5588-gateway-sync.sh--exclude='.github/'(在 --exclude='.git/' 上方)
  3. 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--forcerm -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 一鍵跑:

  1. 備份原 gsync 到 .bak.{timestamp}
  2. 用 awk 在 --exclude='.git/' 上方插入 3 行:
    • --exclude='.smart-env/'(Smart Connections cache)
    • --exclude='.claudian/'(Claudian plugin cache,預防性加入)
    • --exclude='.claude/'(Claude Code 本機設定 + OAuth token,敏感)
  3. chmod +x 保持可執行
  4. 立刻重跑 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 不完整

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 repohttps://github.com/Vincent5588/vincent5588-vault公開(其實是 private)
GitHub secretsrepo → Settings → SecretsCLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID
Workflows.github/workflows/deploy.ymlGitHub Actions build pipeline
Quartz 設定.github/quartz-config/quartz.config.ts主題 / plugins / 字型
Banner plugin.github/quartz-config/draftWarningBanner.tsdraft / deprecated 警告
Custom CSS.github/quartz-config/custom.scss客製樣式
Cloudflare Pageshttps://dash.cloudflare.com → Workers & Pages → vincent5588-wikiHosting
API tokenshttps://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 主目錄