用 Cloudflare 免費方案建立自己的圖庫,真的 $0 起步
你有沒有想過,把自己常用的免費商用圖片集中管理,建一個自己的圖庫入口,搜尋一下就能找到——而且完全不花錢?
我們做了,這篇文章分享整個架構怎麼搭。
先說為什麼要自己建
做設計、做網站、做行銷,免不了找圖。Pixabay、Pexels、Unsplash 都很好用,但每次都要各自打開、各自搜尋,時間碎片化。
更煩的是:你用過一張圖,下次根本不記得在哪找到的。
如果能有一個自己的圖庫入口,把常用來源全部整合進去,搜尋一次橫掃所有來源,那才是省時間的方式。
這就是我們做 FreePicHub 自由圖匯 的出發點。
Cloudflare 免費額度有多夠用
很多人不知道 Cloudflare 有多慷慨。以下是這個專案實際用到的服務,全部在免費方案內:
| 服務 | 用途 | 免費額度 |
|---|---|---|
| R2 物件儲存 | 存圖片檔案 | 10 GB 儲存 / 每月 1,000 萬次 GET |
| D1 資料庫 | 存圖片 metadata | 5 GB 儲存 / 每日 500 萬次讀取 |
| Workers | API 後端 | 每日 10 萬次請求 |
| Pages | 前端網站 | 無限靜態部署 |
一個小型到中型的個人圖庫,這個額度完全夠用,而且不需要信用卡就能開始。
整體架構
[Python 自動化匯入腳本]
↓
圖片處理 (WebP 壓縮)
↓
┌──────────────────┐
│ Cloudflare │
│ │
│ R2 (圖片檔案) │
│ D1 (資料庫) │
│ Workers (API) │
│ Pages (前端) │
└──────────────────┘
↓
瀏覽器使用者四個 Cloudflare 服務各司其職:
- R2:放壓縮過的圖片(縮圖 / 預覽圖 / 原圖)
- D1:SQLite 資料庫,存標題、尺寸、標籤、圖片的 R2 路徑
- Workers:寫兩支 API —
/api/search和/api/featured - Pages:靜態前端,純 HTML + CSS + JS,無框架依賴
Step 1:建立 R2 Bucket
進 Cloudflare Dashboard → R2 → 建立 Bucket,名稱自訂(例如 my-image-library)。
接著到 Manage R2 API Tokens,建立一個 User API Token:
- 權限選「Object Read & Write」
- 限定套用到你的 Bucket
記下 Access Key ID、Secret Access Key、帳號 ID,之後 Python 腳本會用到。
R2 是 S3 相容 API,所以 Python 端用 boto3 就能直接操作,不需要學新的 SDK。
Step 2:建立 D1 資料庫
在 Wrangler CLI 執行:
npx wrangler d1 create my-image-db記下回傳的 database_id。然後建立 Schema:
CREATE TABLE IF NOT EXISTS images (
id TEXT PRIMARY KEY,
source TEXT NOT NULL,
source_id TEXT,
title TEXT,
tags TEXT DEFAULT '[]',
width INTEGER,
height INTEGER,
aspect_ratio TEXT,
license_type TEXT NOT NULL,
thumb_key TEXT,
preview_key TEXT,
original_key TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_source ON images(source);
CREATE INDEX IF NOT EXISTS idx_created ON images(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_source_id ON images(source, source_id);npx wrangler d1 execute my-image-db --file=schema.sqlStep 3:Workers API(用 Pages Functions)
最方便的做法是用 Cloudflare Pages Functions,直接把 JS 放在 /functions 目錄下,Cloudflare 會自動部署成 Worker。
搜尋 API (functions/api/search.js):
export async function onRequestGet({ request, env }) {
const url = new URL(request.url)
const q = url.searchParams.get('q') || ''
const source = url.searchParams.get('source') || ''
const page = parseInt(url.searchParams.get('page') || '1')
const limit = 24
const offset = (page - 1) * limit
let sql = 'SELECT * FROM images WHERE 1=1'
let params = []
if (q) {
sql += ' AND (title LIKE ? OR tags LIKE ?)'
params.push(`%${q}%`, `%${q}%`)
}
if (source) {
sql += ' AND source = ?'
params.push(source)
}
sql += ` ORDER BY created_at DESC LIMIT ${limit + 1} OFFSET ${offset}`
const { results } = await env.MY_DB.prepare(sql).bind(...params).all()
const hasMore = results.length > limit
return Response.json({
results: results.slice(0, limit).map(r => ({
...r,
tags: JSON.parse(r.tags || '[]')
})),
has_more: hasMore
}, { headers: { 'Access-Control-Allow-Origin': '*' } })
}Step 4:圖片處理與自動化匯入
這是整個系統最有趣的部分。寫一支 Python 腳本,把圖片處理後上傳到 R2,metadata 寫入 D1。
圖片處理:每張圖產出三個版本,全部存成 WebP
from PIL import Image
def process_image(source_path, output_dir):
img = Image.open(source_path).convert("RGB")
w, h = img.size
# 縮圖 512px(前端列表用)
thumb = img.copy()
thumb.thumbnail((512, 512), Image.LANCZOS)
thumb.save(output_dir / "thumb.webp", "WEBP", quality=82)
# 預覽圖 1024px(點開 modal 用)
preview = img.copy()
preview.thumbnail((1024, 1024), Image.LANCZOS)
preview.save(output_dir / "preview.webp", "WEBP", quality=85)
# 原圖轉 WebP 壓縮後保存
img.save(output_dir / "original.webp", "WEBP", quality=90)
return {"width": w, "height": h}WebP 格式比 JPEG 通常小 25–35%,長期下來 R2 儲存成本差很多。
上傳到 R2(boto3 S3 相容介面):
import boto3
s3 = boto3.client(
"s3",
endpoint_url=f"https://{ACCOUNT_ID}.r2.cloudflarestorage.com",
aws_access_key_id=ACCESS_KEY,
aws_secret_access_key=SECRET_KEY,
region_name="auto",
)
s3.put_object(Bucket="my-image-library", Key="source/uuid/thumb.webp", Body=file_bytes)寫入 D1(防止重複寫入的關鍵):
import requests, uuid, json
def write_to_d1(record):
resp = requests.post(
f"https://api.cloudflare.com/client/v4/accounts/{ACCOUNT_ID}/d1/database/{DB_ID}/query",
headers={"Authorization": f"Bearer {D1_TOKEN}"},
json={
"sql": """
INSERT INTO images (id, source, source_id, title, tags,
width, height, aspect_ratio, license_type,
thumb_key, preview_key, original_key)
SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
WHERE NOT EXISTS (
SELECT 1 FROM images WHERE source=? AND source_id=?
)
""",
"params": [str(uuid.uuid4()), record["source"], record["source_id"],
record["title"], json.dumps(record["tags"]),
record["width"], record["height"], record["aspect_ratio"],
record["license_type"], record["thumb_key"],
record["preview_key"], record["original_key"],
record["source"], record["source_id"]]
}
)
return resp.json().get("success", False)WHERE NOT EXISTS 讓你重複執行腳本也不會寫入重複資料——這個設計讓整個系統可以安心地每天定時跑,不用擔心跑了幾次。
Step 5:前端部署
前端三個靜態檔案:index.html、style.css、app.js,推上 Cloudflare Pages:
npx wrangler pages deploy ./frontend --project-name my-image-libraryCSS masonry 瀑布流排版,完全不需要 JS 套件:
.image-grid {
columns: 4 220px;
gap: 14px;
}
.image-card {
break-inside: avoid;
margin-bottom: 14px;
}成本試算
以每天匯入 500 張、圖庫規模 10,000 張計算(縮圖約 50KB、預覽圖約 200KB):
| 項目 | 用量 | 費用 |
|---|---|---|
| R2 儲存 | ≈ 2.5 GB | $0 |
| D1 讀取(日訪客 1,000 人) | 5,000 次/日 | $0 |
| Workers 請求 | 5,000 次/日 | $0 |
| Pages 部署 | 無限次 | $0 |
| 總計 | $0 / 月 |
Cloudflare 的免費額度對個人或小型商業圖庫來說幾乎用不完。等規模到幾十萬張、日訪客破萬,再評估要不要升級方案。
小結
整個系統的核心思想很簡單:
- R2 負責存檔案,便宜又快
- D1 負責存索引,搜尋靠它
- Workers 負責查詢邏輯
- Pages 負責呈現給使用者
- Python 腳本 負責定期餵資料進去
全部跑在 Cloudflare edge network 上,全球加速,零維護成本。
如果你也想建一個自己的圖庫,技術門檻其實沒有想像中高——只要會基本的 Python 和 JavaScript,這套架構完全可以在一個週末內跑起來。
實際成品可以參考:FreePicHub 自由圖匯
有技術規格或架構問題想諮詢?
歡迎來 ascentek.info 聯絡我們,無論是 Cloudflare 架構設計、Python 自動化流程,或是圖庫建置規劃,我們很樂意討論。
延伸閱讀