部落格圖片作業流水線 — 零成本、零帳單風險的完整方案
每次做靜態站,圖片管理都是讓人頭大的問題。
WordPress 年代有媒體庫,上傳、壓縮、CDN 全部包辦。切換到 Astro 之後,第一個踩到的坑就是:**圖片到底放哪裡?**我們在設計 Agentic Blog 的時候,列出了所有主流方案,逐一測試風險,最後走出一條零成本、零帳單風險的路。這篇文章把完整設計記錄下來。
我們踩過的坑,以及為什麼不用那些方案
在確定現在的架構之前,我們評估了以下選項:
Cloudflare R2 — 看起來免費,實際上有帳單風險。10 GB 免費,但一旦超過,每 GB 收 $0.015。更麻煩的是,R2 的帳單邏輯是「用了才知道」,監控設定繁瑣,對一個小 blog 來說不值得。
Git LFS — 這是社區公認的陷阱,後面會單獨說明。
Google Drive 直連 — 流量稍大就 403 限流。Google Drive 從來不是 CDN,不應該當 CDN 用。
imgix / Bunny CDN — 根本沒有免費 tier,直接排除。
Cloudinary — 有 25 credits 免費限制,小 blog 大概夠用,但既然 Cloudflare Pages 本身就能做到同樣的事,再多一個第三方帳號只是多一個管理負擔。
最後我們的答案是:Cloudflare Pages 的靜態檔案 CDN 就是我們需要的一切。 帶寬無限免費,全球邊緣節點,自動 HTTPS,不需要額外設定一個字。
1. 全局架構
整個圖片 pipeline 分成五層:來源、Google Drive SSoT、本地作業、Astro Build、CF Pages CDN。
flowchart TB
subgraph 來源["圖片來源"]
CANVA["Canva Pro 素材庫\n夏哥挑選"]
GEMINI["Gemini API 生圖\n$0.039/張(備用)"]
MANUAL["手動上傳\n截圖、照片"]
end
subgraph DRIVE["Google Drive SSoT"]
STOCK["blog-assets/agentic/\nstock-library/\n001.webp ~ 210.webp\n(19.6 MB)"]
end
subgraph 本地["本地作業(SS1 Mac)"]
PULL["rclone pull\n從 Drive 拉素材"]
LOCAL_STOCK["public/images/\nstock-library/\n(.gitignore 不進 repo)"]
SCRIPT["generate-cover.py\n--slug 或 --all"]
MAPPING["cover-mapping.json\n文章↔素材配對表"]
COVER_OUT["public/images/covers/\nagentic-{slug}-cover.png\n(1200×630)"]
BODY_IMG["src/content/posts/\nattachments/\n(文內圖)"]
SATORI["Satori OG Endpoint\nsrc/pages/og/[...slug].png.ts\n(1200×630 自動生成)"]
end
subgraph BUILD["Astro Build"]
SHARP["Sharp 圖片優化\nPNG/JPG → WebP\n7 個響應式尺寸"]
DIST["dist/\n部署產物"]
end
subgraph CDN["Cloudflare Pages CDN"]
EDGE["全球邊緣節點\n免費無限帶寬\n自動 HTTPS"]
READER["讀者瀏覽器\n最近節點取圖"]
end
CANVA -->|"下載 zip"| STOCK
GEMINI -->|"API 輸出"| STOCK
MANUAL -->|"上傳"| STOCK
STOCK -->|"rclone pull"| PULL
PULL --> LOCAL_STOCK
LOCAL_STOCK --> SCRIPT
MAPPING --> SCRIPT
SCRIPT -->|"PIL 合成\n素材圖 + 暗色 overlay\n+ 標題 + 分類 pill\n+ 品牌水印"| COVER_OUT
COVER_OUT -->|"進 repo"| SHARP
BODY_IMG -->|"進 repo"| SHARP
SATORI -->|"build-time\n自動生成"| DIST
SHARP -->|"優化壓縮"| DIST
DIST -->|"wrangler pages deploy\n或 git push → 自動 build"| EDGE
EDGE --> READER
關鍵設計決策:素材圖(stock-library)不進 repo,透過 .gitignore 排除,只有最終合成的 cover PNG 才進 repo。這樣 repo 只存放真正需要版本控制的產出物,不會因為原始素材庫膨脹而拖累 git 效能。
2. 三種圖片類型
Agentic Blog 的圖片分三類,各自有不同的存放路徑、生成方式和 CDN 行為:
| 類型 | 尺寸 | 格式 | 存放路徑 | 進 repo | 生成方式 |
|---|---|---|---|---|---|
| Cover 封面 | 1200×630 | PNG | public/images/covers/ | 是 | generate-cover.py |
| OG 社群分享 | 1200×630 | PNG | dist/og/(build 產物) | 否 | Satori(build-time 自動) |
| Body 文內圖 | 不限 | PNG/JPG/WebP | src/content/posts/attachments/ | 是 | VaultCMS / 手動 |
| Stock 素材 | 1200×630 | WebP | Google Drive | 否(.gitignore) | Canva Pro 挑選 |
Cover 圖是唯一需要手動觸發生成的類型。OG 圖由 Satori 在 astro build 時自動生成,完全不需要人工介入。Body 圖直接放進 attachments/ 資料夾,由 Astro 的 Sharp integration 在 build 時優化成 7 個響應式尺寸。
3. Cover 生成流程
封面圖是整個 pipeline 中最有意思的環節。我們不用 AI 生圖(成本和一致性都是問題),而是從 Canva Pro 素材庫挑選 210 張高品質 WebP 作為原料,再用 generate-cover.py 批量合成風格一致的封面。
sequenceDiagram
participant 夏哥
participant Canva as Canva Pro
participant Drive as Google Drive
participant Agent as 小克 (Agent)
participant Script as generate-cover.py
participant Repo as Git Repo
participant CF as CF Pages CDN
夏哥->>Canva: 搜尋素材圖
夏哥->>Canva: 挑選 + 下載 zip
夏哥->>Drive: 上傳到 blog-assets/agentic/stock-library/
Note over 夏哥: 以上一次性(已完成 210 張)
夏哥->>Agent: 寫新文章
Agent->>Agent: rclone pull Drive → 本地 stock-library/
Agent->>Agent: 編輯 cover-mapping.json(配對素材編號)
Agent->>Script: python3 generate-cover.py --slug {slug}
Script->>Script: 載入素材圖(WebP/PNG)
Script->>Script: Resize + Center Crop → 1200×630
Script->>Script: 漸層暗色 Overlay(上淺下深)
Script->>Script: 4px Terra Cotta 左色條
Script->>Script: 分類 Pill(專案身份色)
Script->>Script: 標題文字(PingFang 48px)
Script->>Script: SUPERPORTIA AGENTIC 水印
Script-->>Agent: 輸出 PNG → public/images/covers/
Agent->>Repo: git add + commit
Repo->>CF: push → 自動 build + deploy
CF-->>夏哥: 文章上線,封面圖從最近邊緣節點載入
generate-cover.py 做的事情:
- 從
cover-mapping.json讀取 slug → 素材編號的配對 - 用 PIL 把素材圖 resize + center crop 到 1200×630
- 疊加漸層暗色 overlay(上方較透明、下方較深,讓文字可讀)
- 在左側畫 4px Terra Cotta 色條(品牌識別)
- 根據文章分類畫對應身份色的分類 pill
- 渲染標題文字(PingFang SC,48px)
- 加上 SUPERPORTIA AGENTIC 品牌水印
- 輸出到
public/images/covers/agentic-{slug}-cover.png
單次執行指令:
# 生成單篇文章的封面
python3 scripts/generate-cover.py --slug agentic-blog-image-pipeline
# 批量生成所有文章的封面(初次設定或素材更新後)
python3 scripts/generate-cover.py --all
# 從 Drive 拉取最新素材(需先設定 rclone)
rclone copy gdrive:blog-assets/agentic/stock-library/ public/images/stock-library/
命名規則保持一致:
| 類型 | 格式 | 範例 |
|---|---|---|
| Stock 素材 | {NNN}.webp | 001.webp、210.webp |
| Cover 封面 | agentic-{slug}-cover.png | agentic-egs-engineering-governance-spec-cover.png |
| OG 圖 | /og/{slug}.png | /og/egs-engineering-governance-spec.png |
| Body 文內圖 | {描述}.{ext} | mtaaa-architecture.png |
4. 為什麼不用 R2、Git LFS、Google Drive 直連
這是整個設計中最重要的決策之一,值得詳細說明。
Git LFS 提供每月 1 GB 免費帶寬。問題是,這 1 GB 是所有協作者的 clone + fetch 合計消耗。 一個有 50 MB 圖片的 repo,只要 20 次 clone 就會超過免費額度,然後 GitHub 會自動向你的帳戶收費,沒有警告,沒有停止鍵。社區裡有無數人因為 Git LFS 的隱性帳單損失幾十到幾百美金。這不是誇張,是有據可查的設計缺陷。
| 方案 | 表面優點 | 真實風險 |
|---|---|---|
| CF R2 | 10 GB 免費,零出口費 | 超限自動收費,監控繁瑣 |
| Git LFS | 原生 git 支援 | 帶寬超限自動收費,社區公認陷阱 |
| GitHub raw 直連 | 不需額外設定 | 2025 起限流嚴重,違反 ToS |
| Google Drive 直連 | 免費 | 流量稍大就 403,不穩定 |
| Cloudinary | CDN 功能完整 | 25 credits 限制,多一個第三方帳號 |
CF Pages 靜態檔案 CDN 的優勢在於:超過限制時的行為是「拒絕部署」而不是「繼續服務並收費」。 這是本質上的安全差異。20,000 檔的免費上限到達後,wrangler 會報錯、部署停止,不會悄悄升級你的方案。
5. 費用分析:每個環節都是 $0
| 項目 | 費用 | 免費額度 | 超限行為 |
|---|---|---|---|
| CF Pages 帶寬 | $0 | 無限 | 不適用 |
| CF Pages 檔案數 | $0 | 20,000 檔 | 部署停止(不收費) |
| CF Pages 建構次數 | $0 | 500 次/月 | 排隊等待(不收費) |
| Google Drive 儲存 | $0 | 15 GB | 無法上傳(不收費) |
| PIL / generate-cover.py | $0 | 本地執行 | 不適用 |
| Satori OG 生成 | $0 | build-time | 不適用 |
| Sharp 圖片優化 | $0 | Astro 內建 | 不適用 |
| 總計 | $0/月 | 零帳單風險 |
目前架構中,每一個超限事件的後果都是「服務停止」而不是「帳單增加」。這是我們選擇這套方案的核心原因:不需要盯著 billing dashboard,也不需要設定帳單警報。
6. 監控清單
雖然不會意外收費,但仍需要定期確認各項指標沒有接近警戒線:
| 監控項目 | 指令 | 免費上限 | 警戒線 | 超限後果 | 處理方式 |
|---|---|---|---|---|---|
| Git repo 大小 | git count-objects -vH | GitHub 建議 < 1 GB | 500 MB | push 變慢 | 搬圖到 R2 |
| CF Pages 檔案數 | CF Dashboard → Pages | 20,000 檔 | 15,000 | 部署失敗 | 減少 Sharp 尺寸或搬 R2 |
| CF Pages 建構次數 | CF Dashboard → Pages | 500 次/月 | 400 | 排隊等待 | 減少無效 push |
| Google Drive 空間 | Drive 設定頁 | 15 GB | 12 GB | 無法上傳 | 清理或升級 |
| 單張圖片大小 | ls -lh public/images/covers/ | CF Pages 限 25 MB/檔 | 5 MB | 部署失敗 | 壓縮 |
| Build 時間 | astro build 輸出 | 無硬限 | 5 分鐘 | 開發效率下降 | 清 Sharp cache |
建議每月跑一次以下腳本,輸出簡短的健康報告:
#!/bin/bash
# check-image-health.sh — 每月跑一次
echo "= Agentic Blog 圖片管線健康檢查 ="
echo ""
echo "Repo 大小:"
git count-objects -vH | grep size-pack
echo ""
echo "Cover 圖數量: $(ls public/images/covers/*.png 2>/dev/null | wc -l | tr -d ' ') 張"
echo "Stock 素材數量: $(ls public/images/stock-library/*.webp 2>/dev/null | wc -l | tr -d ' ') 張"
echo "Body 圖數量: $(find src/content/posts/attachments/ -type f 2>/dev/null | wc -l | tr -d ' ') 張"
echo ""
echo "public/images 總大小: $(du -sh public/images/ 2>/dev/null | cut -f1)"
echo ""
echo "最大的 5 張 Cover 圖:"
ls -lhS public/images/covers/*.png 2>/dev/null | head -5 | awk '{print $5, $9}'
7. 容量規劃
根據目前的發文節奏(每週 2-3 篇),我們做了以下估算:
| 階段 | 文章數 | Cover 大小 | Body 圖估計 | Repo 總增量 | 需要遷移? |
|---|---|---|---|---|---|
| 現在(2026-03) | 25 | 12.5 MB | ~5 MB | ~18 MB | 不需要 |
| 6 個月後 | 100 | 50 MB | ~30 MB | ~80 MB | 不需要 |
| 1 年後 | 200 | 100 MB | ~60 MB | ~160 MB | 接近考慮點 |
| 2 年後 | 500 | 250 MB | ~150 MB | ~400 MB | 考慮搬 R2 |
遷移觸發點:repo 超過 500 MB 或源圖超過 200 張時,搬到 CF R2(零出口費)。
CF R2 在遷移發生後依然是安全選項:零出口費意味著圖片被讀取不產生費用,超過 10 GB 免費額度才以 $0.015/GB 計費,而那個時候 blog 應該已經有足夠的商業理由支撐這點小額費用。
8. 我們的觀點
比 Cloudinary 簡單 — 不需要額外帳號,不需要 SDK,不需要 API key 管理。圖片就是靜態檔案,放進 repo,CF Pages 自動 CDN。
比 R2 安全 — R2 超限收費,CF Pages 超限停止部署。「停止」比「收費」安全,特別是對個人 blog 這種流量不可預測的場景。
比 Git LFS 可靠 — Git LFS 的帶寬帳單是開發者社區記錄最多的「驚喜帳單」來源之一。我們的方案把大型素材圖(stock-library)放在 Google Drive 並 .gitignore 掉,只有最終合成的 cover PNG 進 repo,大小可控。
CF Pages 免費 CDN 的持久性 — 這不是 Cloudflare 的隨機 free tier,是他們吸引開發者、建立生態系的戰略性產品。Cloudflare 的商業模式是賣企業方案,不是靠個人開發者的 CDN 費用。Pages 的免費 tier 已經穩定存在數年,風險遠低於同類競品。
工具清單
| 工具 | 用途 | 路徑 / 指令 |
|---|---|---|
generate-cover.py | 素材圖 → Cover PNG | scripts/generate-cover.py --slug {slug} |
cover-mapping.json | 文章↔素材配對 | scripts/cover-mapping.json |
| Satori endpoint | OG 圖 build-time 自動生成 | src/pages/og/[...slug].png.ts |
| rclone | Drive ↔ 本地素材同步 | rclone copy gdrive:blog-assets/agentic/stock-library/ public/images/stock-library/ |
| Sharp(Astro 內建) | Build-time 圖片優化,PNG/JPG → WebP × 7 尺寸 | 自動(astro build) |
check-image-health.sh | 每月健康檢查 | bash scripts/check-image-health.sh |
相關文件
- Agentic Blog Image Pipeline — 本文對應的 vault spec(完整規格)
- Image Pipeline — Complete Rules & Recommendation — 早期規劃(已被現行設計取代)
- Astro Modular Components — 圖片相關元件(ImageWrapper、ImageGallery、Lightbox)
- Google Drive Image SSoT — Drive 資料夾結構詳細說明
- Agentic — 專案 MOC
...有限 未解決的問題: 圖片貼文是最大的缺口。12 位 KOL 裡至少有 4 位習慣用截圖分享觀點,這些內容目前完全無法進入 NLM 分析。解法需要 OCR + 圖片轉文字的前處理步驟,這是 [[Agentic Blog Image Pipeline]] 的延伸問題。 下一步 三個優先執行項目: 1. 建立 KOL-STT...