sell整合實戰
rss_feed

架構藍圖:部落格 / CMS

看 Pages、Workers、D1、R2、KV 與 Images 如何拼成一個真正的內容網站。

6Cloudflare 產品
4資料表
1Worker API
~0ms邊緣快取讀取
insights

我們要蓋什麼?

一個部落格或 CMS(內容管理系統,就是負責存放與供應文章的軟體)要做好四件事:把頁面顯示給讀者、跑一個 API、存放文字、存放圖片。Cloudflare 不是用一台大伺服器全包,而是把這些工作拆給數個小而專一的服務,全部都跑在邊緣(edge,指離讀者很近的資料中心)。

這一頁是「一眼看懂整個系統」的地圖。下面這張圖把每個零件串起來:讀者的瀏覽器同時跟 Pages(前端)和 Worker(API)對話;Worker 從 D1 讀文章、從 R2 讀圖片、從 KV 拿快取;而瀏覽器則透過 Images 抓最佳化過的圖片。

schema系統架構

頁面 HTML

fetch /api

呼叫

SQL 查詢

快取讀寫

物件鍵

img src

回源讀取

瀏覽器

Pages 前端

Worker API

D1 文章與作者

KV 快取

R2 圖片

Images 縮圖

restaurant

把它想成一間餐廳

Pages 是用餐區(客人看到的),Worker 是服務生(接單、上菜),D1 是食譜本(結構化的文字),R2 是儲藏室(原始食材/檔案),KV 是出餐檯上保溫的菜(馬上再上一份),而 Images 是擺盤台(依需求把每張照片調整大小)。

account_tree

各零件的職責

每個 Cloudflare 產品都只扮演一個明確的角色。把它們分開,正是讓系統便宜、快、又好理解的關鍵——你可以只動快取而不碰資料庫,或換掉前端卻不用重寫 API。

web

Pages — 前端

託管你的 HTML、CSS 與 JavaScript,把部落格介面送到全球。這就是讀者實際在瀏覽器裡打開的東西。

bolt

Workers — API

大腦。它接收 /api 請求,決定要讀或寫什麼,跟 D1、R2、KV 對話,最後回傳 JSON。你所有的商業邏輯都住在這裡。

database

D1 — 結構化資料

一個 SQL 資料庫,存放文章、作者與標籤——凡是有關聯、要用 SELECT/INSERT 查詢的資料都放這。文字的真實來源。

inventory_2

R2 — 檔案儲存

存放大型二進位檔案的物件儲存:封面照、上傳檔、附件。資料庫只存物件鍵(key),真正的位元組住在這裡。

cached

KV — 讀取快取

一個超快的鍵值(key→value)儲存,用來記住熱門文章的 JSON,讓重複讀取完全跳過資料庫。文章一變更就清掉。

auto_fix_high

Images — 圖片最佳化

直接從一個網址即時縮放、裁切、轉檔照片,讓每台裝置都拿到合適的尺寸,你不必自己存好幾份副本。

schema

資料模型

四張表就能描述一個部落格。一位作者寫多篇文章。一篇文章可以掛多個標籤,一個標籤也能出現在多篇文章上——這種「多對多」關係需要一張中介表,叫 post_tags。注意 posts 只存 cover_key(指向 R2 的指標),不存圖片本身的位元組。

schema實體關聯圖

撰寫

擁有

標記

AUTHORS

int

id

PK

text

name

text

email

POSTS

int

id

PK

int

author_id

FK

text

title

text

slug

text

cover_key

int

published

POST_TAGS

int

post_id

FK

int

tag_id

FK

TAGS

int

id

PK

text

name

text

slug

sqlschema.sql
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)
);
swap_vert

讀取與發布流程

讀一篇文章(含快取)

有人打開一篇文章時,Worker 會先看 KV。如果 JSON 已經在那裡(命中快取),就立刻回傳、完全不碰資料庫。如果不在(未命中),就去查 D1、組好回應,再存進 KV 給下次用。這就是為什麼熱門文章能一直又快又便宜。

schema讀取流程:KV 命中 vs 未命中
"D1""KV 快取""Worker API""瀏覽器""D1""KV 快取""Worker API""瀏覽器"alt[命中快取][未命中]GET /api/posts/hello讀取快取鍵已快取 JSONJSON x-cache=HIT空值查詢文章與作者資料列寫入快取 TTL=300sJSON x-cache=MISS

發布一篇文章

寫入則是反方向。作者送出文字加上一張封面圖;Worker 把檔案存進 R2、把資料列(和它的標籤)寫進 D1,然後刪掉 KV 裡的快取副本,讓讀者馬上看到新版本。

schema發布流程
"KV 快取""D1""R2""Worker API""作者""KV 快取""D1""R2""Worker API""作者"POST /api/posts + 圖片儲存封面圖片物件鍵寫入 post 與 post_tags新文章 id刪除快取文章201 Created
construction

動手做:綁定與程式碼

綁定(binding)是一個有名字的連接器(例如 DB 或 CACHE),你在 wrangler.jsonc 裡宣告它,Worker 就能用 env.<名稱> 存取資源——程式碼裡不用放連線字串或密鑰。以下:建立資源、宣告綁定、定義結構,再寫 Worker 與前端。

  1. 建立儲存資源

    資料庫、快取命名空間、圖片桶各一條指令。把 Wrangler 印出的 ID 複製起來。

    bash
    npx wrangler d1 create blog
    npx wrangler kv namespace create CACHE
    npx wrangler r2 bucket create blog-images
  2. 宣告綁定

    把三個綁定都放進 wrangler.jsonc。這樣 Worker 裡就能用 env.DB、env.CACHE 與 env.BUCKET。

    json
    {
      "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" }
      ]
    }
  3. 套用資料庫結構

    用 --remote 把「資料模型」段落裡的 schema.sql 套到雲端上真正的資料庫。

    bash
    npx wrangler d1 execute blog --remote --file=./schema.sql
  4. 寫 Worker API(讀取路徑)

    這個 handler 是整張藍圖的核心:先 KV、再 D1、最後寫回 KV。它也用 R2 的 cover_key 組出 Images 網址。

    js
    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" },
        });
      },
    };
  5. 串接前端

    在 Pages 上,只要用原生 fetch() 打 Worker API 就夠了。頁面把標題、作者與最佳化過的封面圖渲染出來。

    js
    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());
  6. 部署上線

    發布 Worker;把你的 repo 接上 Pages 做前端。整個部落格就上線在 Cloudflare 的邊緣了。

    bash
    npx wrangler deploy
school

重點概念

cable

用綁定,不用連線字串

你用 env.DB/env.CACHE/env.BUCKET 存取 D1、KV、R2。程式碼裡沒有任何密碼會外洩。

key

快取鍵與 TTL

每篇文章以 post:<slug> 為鍵快取,並設定 TTL(存活時間)——就算沒人去清,300 秒後也會自動過期。

delete_sweep

寫入時失效

文章一變更就刪掉它的 KV 鍵。否則在 TTL 到期前,讀者會一直看到過時的快取版本。

category

依資料型態選對工具

關聯式文字 → D1;大型檔案 → R2;熱讀快取 → KV。混用(例如把圖片塞進 D1)會又慢又貴。

tune

用網址處理圖片

在網址加上 /cdn-cgi/image/width=1200/ 就能即時縮放——R2 只存一份原圖,所有尺寸都由它供應。

dynamic_feed

Pages 與 Worker 分工

Pages 供應靜態介面;Worker 供應 /api。兩者各自部署,所以調整介面絕不會波及你的 API。

tips_and_updates

小提示與陷阱

payments

免費額度疊起來很夠用

D1(每天 500 萬列讀取)、KV(每天 10 萬次讀取)、R2(10GB 且免下載流量費)與 Pages(無限靜態請求)各自都有免費額度——小型部落格幾乎零成本就能跑。有了快取,大多數讀取根本用不到 D1 的額度。

  • 編輯或下架文章時一定要讓 KV 鍵失效,否則讀者會看到過時內容。
  • D1 裡只存 cover_key(一個短字串);真正的圖片位元組放在 R2。
  • 在 posts(slug) 上建索引——它是你主要的查找欄位,能讓讀取一直很便宜。
  • R2 沒有下載流量費,所以供應圖片比一般雲端儲存便宜很多。
  • 密鑰(例如管理者 token)放在 wrangler secrets,千萬別寫進 wrangler.jsonc。