架構藍圖:部落格 / CMS
看 Pages、Workers、D1、R2、KV 與 Images 如何拼成一個真正的內容網站。
我們要蓋什麼?
一個部落格或 CMS(內容管理系統,就是負責存放與供應文章的軟體)要做好四件事:把頁面顯示給讀者、跑一個 API、存放文字、存放圖片。Cloudflare 不是用一台大伺服器全包,而是把這些工作拆給數個小而專一的服務,全部都跑在邊緣(edge,指離讀者很近的資料中心)。
這一頁是「一眼看懂整個系統」的地圖。下面這張圖把每個零件串起來:讀者的瀏覽器同時跟 Pages(前端)和 Worker(API)對話;Worker 從 D1 讀文章、從 R2 讀圖片、從 KV 拿快取;而瀏覽器則透過 Images 抓最佳化過的圖片。
把它想成一間餐廳
Pages 是用餐區(客人看到的),Worker 是服務生(接單、上菜),D1 是食譜本(結構化的文字),R2 是儲藏室(原始食材/檔案),KV 是出餐檯上保溫的菜(馬上再上一份),而 Images 是擺盤台(依需求把每張照片調整大小)。
各零件的職責
每個 Cloudflare 產品都只扮演一個明確的角色。把它們分開,正是讓系統便宜、快、又好理解的關鍵——你可以只動快取而不碰資料庫,或換掉前端卻不用重寫 API。
Pages — 前端
託管你的 HTML、CSS 與 JavaScript,把部落格介面送到全球。這就是讀者實際在瀏覽器裡打開的東西。
Workers — API
大腦。它接收 /api 請求,決定要讀或寫什麼,跟 D1、R2、KV 對話,最後回傳 JSON。你所有的商業邏輯都住在這裡。
D1 — 結構化資料
一個 SQL 資料庫,存放文章、作者與標籤——凡是有關聯、要用 SELECT/INSERT 查詢的資料都放這。文字的真實來源。
R2 — 檔案儲存
存放大型二進位檔案的物件儲存:封面照、上傳檔、附件。資料庫只存物件鍵(key),真正的位元組住在這裡。
KV — 讀取快取
一個超快的鍵值(key→value)儲存,用來記住熱門文章的 JSON,讓重複讀取完全跳過資料庫。文章一變更就清掉。
Images — 圖片最佳化
直接從一個網址即時縮放、裁切、轉檔照片,讓每台裝置都拿到合適的尺寸,你不必自己存好幾份副本。
資料模型
四張表就能描述一個部落格。一位作者寫多篇文章。一篇文章可以掛多個標籤,一個標籤也能出現在多篇文章上——這種「多對多」關係需要一張中介表,叫 post_tags。注意 posts 只存 cover_key(指向 R2 的指標),不存圖片本身的位元組。
CREATE TABLE authors (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
CREATE TABLE posts (
id INTEGER PRIMARY KEY,
author_id INTEGER NOT NULL REFERENCES authors(id),
title TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
body TEXT NOT NULL,
cover_key TEXT,
published INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE tags (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL
);
CREATE TABLE post_tags (
post_id INTEGER NOT NULL REFERENCES posts(id),
tag_id INTEGER NOT NULL REFERENCES tags(id),
PRIMARY KEY (post_id, tag_id)
);讀取與發布流程
讀一篇文章(含快取)
有人打開一篇文章時,Worker 會先看 KV。如果 JSON 已經在那裡(命中快取),就立刻回傳、完全不碰資料庫。如果不在(未命中),就去查 D1、組好回應,再存進 KV 給下次用。這就是為什麼熱門文章能一直又快又便宜。
發布一篇文章
寫入則是反方向。作者送出文字加上一張封面圖;Worker 把檔案存進 R2、把資料列(和它的標籤)寫進 D1,然後刪掉 KV 裡的快取副本,讓讀者馬上看到新版本。
動手做:綁定與程式碼
綁定(binding)是一個有名字的連接器(例如 DB 或 CACHE),你在 wrangler.jsonc 裡宣告它,Worker 就能用 env.<名稱> 存取資源——程式碼裡不用放連線字串或密鑰。以下:建立資源、宣告綁定、定義結構,再寫 Worker 與前端。
建立儲存資源
資料庫、快取命名空間、圖片桶各一條指令。把 Wrangler 印出的 ID 複製起來。
npx wrangler d1 create blog npx wrangler kv namespace create CACHE npx wrangler r2 bucket create blog-images宣告綁定
把三個綁定都放進 wrangler.jsonc。這樣 Worker 裡就能用 env.DB、env.CACHE 與 env.BUCKET。
{ "name": "blog-api", "main": "src/index.js", "compatibility_date": "2025-01-01", "d1_databases": [ { "binding": "DB", "database_name": "blog", "database_id": "<your-d1-id>" } ], "kv_namespaces": [ { "binding": "CACHE", "id": "<your-kv-id>" } ], "r2_buckets": [ { "binding": "BUCKET", "bucket_name": "blog-images" } ] }套用資料庫結構
用 --remote 把「資料模型」段落裡的 schema.sql 套到雲端上真正的資料庫。
npx wrangler d1 execute blog --remote --file=./schema.sql寫 Worker API(讀取路徑)
這個 handler 是整張藍圖的核心:先 KV、再 D1、最後寫回 KV。它也用 R2 的 cover_key 組出 Images 網址。
export default { async fetch(request, env) { const url = new URL(request.url); const slug = url.pathname.split("/").pop(); const cacheKey = `post:${slug}`; // 1) Try the KV cache first const cached = await env.CACHE.get(cacheKey); if (cached) { return new Response(cached, { headers: { "content-type": "application/json", "x-cache": "HIT" }, }); } // 2) Cache miss -> read post + author from D1 const post = await env.DB .prepare( "SELECT p.id, p.title, p.body, p.cover_key, a.name AS author " + "FROM posts p JOIN authors a ON a.id = p.author_id " + "WHERE p.slug = ? AND p.published = 1" ) .bind(slug) .first(); if (!post) return new Response("Not found", { status: 404 }); // 3) Build an optimised image URL (R2 object served via Images) post.cover_url = `https://img.example.com/cdn-cgi/image/width=1200/${post.cover_key}`; const json = JSON.stringify(post); // 4) Save to KV for 5 minutes so the next read is instant await env.CACHE.put(cacheKey, json, { expirationTtl: 300 }); return new Response(json, { headers: { "content-type": "application/json", "x-cache": "MISS" }, }); }, };串接前端
在 Pages 上,只要用原生 fetch() 打 Worker API 就夠了。頁面把標題、作者與最佳化過的封面圖渲染出來。
async function loadPost(slug) { const res = await fetch(`/api/posts/${slug}`); if (!res.ok) throw new Error("Post not found"); const post = await res.json(); document.querySelector("#title").textContent = post.title; document.querySelector("#author").textContent = "by " + post.author; document.querySelector("#cover").src = post.cover_url; document.querySelector("#body").innerHTML = post.body; } // Read the slug from the URL, e.g. /blog/hello-world loadPost(location.pathname.split("/").pop());部署上線
發布 Worker;把你的 repo 接上 Pages 做前端。整個部落格就上線在 Cloudflare 的邊緣了。
npx wrangler deploy
重點概念
用綁定,不用連線字串
你用 env.DB/env.CACHE/env.BUCKET 存取 D1、KV、R2。程式碼裡沒有任何密碼會外洩。
快取鍵與 TTL
每篇文章以 post:<slug> 為鍵快取,並設定 TTL(存活時間)——就算沒人去清,300 秒後也會自動過期。
寫入時失效
文章一變更就刪掉它的 KV 鍵。否則在 TTL 到期前,讀者會一直看到過時的快取版本。
依資料型態選對工具
關聯式文字 → D1;大型檔案 → R2;熱讀快取 → KV。混用(例如把圖片塞進 D1)會又慢又貴。
用網址處理圖片
在網址加上 /cdn-cgi/image/width=1200/ 就能即時縮放——R2 只存一份原圖,所有尺寸都由它供應。
Pages 與 Worker 分工
Pages 供應靜態介面;Worker 供應 /api。兩者各自部署,所以調整介面絕不會波及你的 API。
小提示與陷阱
免費額度疊起來很夠用
D1(每天 500 萬列讀取)、KV(每天 10 萬次讀取)、R2(10GB 且免下載流量費)與 Pages(無限靜態請求)各自都有免費額度——小型部落格幾乎零成本就能跑。有了快取,大多數讀取根本用不到 D1 的額度。
- 編輯或下架文章時一定要讓 KV 鍵失效,否則讀者會看到過時內容。
- D1 裡只存 cover_key(一個短字串);真正的圖片位元組放在 R2。
- 在 posts(slug) 上建索引——它是你主要的查找欄位,能讓讀取一直很便宜。
- R2 沒有下載流量費,所以供應圖片比一般雲端儲存便宜很多。
- 密鑰(例如管理者 token)放在 wrangler secrets,千萬別寫進 wrangler.jsonc。