這篇是這個網站自身的部署紀錄。SvelteKit 2 跑在 Cloudflare Pages,資料庫用 D1,靜態檔案(封面、圖片)放 R2

整套組合對個人博客來說基本上可以壓在 Cloudflare 的免費額度內,目前我的帳單是 $0。R2 本身沒有 Internet egress fee,但儲存與操作仍然有計費規則,以個人博客的規模來說通常不太容易碰到付費門檻。

如果你也想把 SvelteKit 丟上 Cloudflare,照著這篇做就對了。我把這次部署過程中的幾個坑也一起記下來,之後自己要重新部署也比較方便。

為什麼選 Cloudflare

服務 成本 用途
Pages $0 部署與 SSR(Edge 渲染)
Workers $0(方案內) 函式後端
D1 $0 SQLite 資料庫
R2 接近 $0 物件儲存(圖片等靜態檔)

對個人博客這種流量來說,這幾項基本上都可以在免費額度內。

而且最重要的其實不是單純免費,而是不用自己維護伺服器。

不用自己處理 TLS 憑證、Nginx、Server 更新,也不用另外維護一台 VPS。SvelteKit 的 SSR 直接跑在 Cloudflare 的 Edge 環境,D1 負責資料,R2 負責圖片,整體架構也很單純。

所以最後的結構大概就是:

SvelteKit
    │
    ▼
Cloudflare Pages
    ├── D1  → 資料庫
    │
    └── R2  → 圖片、封面等靜態檔案

對個人博客來說,我覺得這樣已經很夠用了。


第一步:換掉 adapter-auto

SvelteKit 預設的 adapter-auto 在 Pages 上也能跑,但如果要使用 D1、R2 這類 Cloudflare binding,platform 的注入、binding 型別,以及本地開發環境的 Cloudflare runtime 都需要另外處理。

所以這邊直接換成 adapter-cloudflare

pnpm add -D @sveltejs/adapter-cloudflare

然後修改 vite.config.ts

import adapter from '@sveltejs/adapter-cloudflare';

export default defineConfig({
	plugins: [
		sveltekit({
			adapter: adapter({
				config: 'wrangler.toml',
				platformProxy: {
					configPath: 'wrangler.toml',
					persist: true
				}
			})
		})
	]
});

這裡 platformProxy 比較重要:

platformProxy: {
	configPath: 'wrangler.toml',
	persist: true
}

它會讓 pnpm dev 的時候也可以取得 Cloudflare 的 binding。

也就是說,本地開發時可以直接使用 D1 / R2,不需要另外開一套本地服務。

persist: true 則是讓本地使用的資料持續存在,不然重新啟動開發環境後,本地 D1 的資料可能需要重新處理。

這對需要反覆測試資料庫的博客後台來說會方便很多。


第二步:wrangler.toml 綁定

接下來設定 Cloudflare 的 binding:

name = "kirikirin-blog"
compatibility_date = "2026-08-20"
pages_build_output_dir = ".svelte/cloudflare"

[[d1_databases]]
binding = "DB"
database_name = "kirikirin-blog"
database_id = "<從 wrangler d1 create 取得>"

[[r2_buckets]]
binding = "BUCKET"
bucket_name = "blog-assets"

這裡比較容易搞混的是 binding 和實際的資料庫/bucket 名稱。

例如:

binding = "DB"
database_name = "kirikirin-blog"

database_name 是 Cloudflare 上的 D1 名稱,而 binding = "DB" 是程式裡取得這個資料庫時使用的名稱。

所以最後在 SvelteKit 裡會是:

platform.env.DB

R2 也是一樣:

binding = "BUCKET"
bucket_name = "blog-assets"

程式裡就是:

platform.env.BUCKET

也就是:

wrangler.toml

binding = "DB"
      ↓
platform.env.DB

建立 D1

如果還沒有建立 D1,可以直接:

pnpm wrangler d1 create kirikirin-blog

執行之後 Wrangler 會回傳 database ID,把它填到:

database_id = "..."

就可以了。


⚠️ ASSETS 是 Pages 的保留 binding 名稱

這是這次比較容易踩到的一個坑。

ASSETS 是 Cloudflare Pages 使用的保留 binding 名稱,所以不要拿 ASSETS 當自己的 D1 / R2 binding。

例如:

binding = "ASSETS"

可能會導致部署失敗,而且錯誤訊息並不是很直觀。

所以自己的 binding 直接取:

binding = "DB"

和:

binding = "BUCKET"

會比較簡單。


D1 的 Schema 變更方式

D1 本身就是 SQLite,所以 Schema migration 沒有必要搞得太複雜。

目前這邊直接把 migration SQL 放在:

migrations/
├── 0001_xxx.sql
├── 0002_xxx.sql
└── ...

需要修改 Schema 就新增一個 migration。

例如:

ALTER TABLE posts ADD COLUMN slug TEXT;

然後直接執行:

pnpm wrangler d1 execute kirikirin-blog --remote --file=migrations/0002_xxx.sql

把 migration 檔案留在 migrations/ 資料夾,就是最陽春的版本控制。

對個人博客來說其實已經夠了。

而且 migration 檔案最好跟著 Git 一起保存。這些檔案就是 Schema 的歷史,刪掉之後之後要重新建立環境、追查 Schema 變更時會很麻煩。


第三步:型別聲明

接下來是 TypeScript 型別。

src/app.d.ts

import type { D1Database, R2Bucket } from '@cloudflare/workers-types';

declare global {
	namespace App {
		interface Platform {
			env: {
				DB: D1Database;
				BUCKET: R2Bucket;
			};
		}
	}
}

有了這個之後:

platform?.env.DB

就會有完整的 D1 型別提示。

R2 也是:

platform?.env.BUCKET

不過我自己不太喜歡在專案裡到處使用 platform?.env

實際上可以另外包一層 getDb() / getBucket() wrapper,把 binding 的取得集中起來。

例如:

function getDb(platform: App.Platform | undefined) {
	if (!platform?.env.DB) {
		throw new Error('D1 database is not configured');
	}

	return platform.env.DB;
}

這樣 DB 沒有正確配置時,錯誤也比較容易定位。


部署

到這裡基本上就可以部署了。

有兩種方式。

1. GitHub 串接

把 repository 直接連上 Cloudflare Pages,push 之後自動部署。

Build 指令:

pnpm build

輸出目錄:

.svelte/cloudflare

這種方式最省事,之後正常 git push 就會自動部署。


2. 手動指令

我目前用的是這種:

pnpm wrangler pages deploy .svelte/cloudflare --project-name kirikirin-blog

手動部署的好處是部署前可以先自己跑:

pnpm lint
pnpm check
pnpm build

確認都沒有問題之後再 deploy。

博客還在開發階段的話,我自己覺得這樣比較方便。


⚠️ Schema 改完記得先跑 migration

這個算是部署時比較容易忘記的地方。

如果程式碼改了資料庫 Schema,例如新增 table 或欄位,部署前要先確認 production D1 已經完成 migration。

不然就很容易變成:

新程式碼
   ↓
production D1 還是舊 Schema
   ↓
500

所以比較簡單的流程就是:

修改 Schema
    ↓
新增 migration
    ↓
執行 D1 migration
    ↓
lint / check / build
    ↓
deploy

如果 Schema 的修改比較複雜,還是要考慮新舊程式碼之間的相容性,不要讓 production 在 migration 的過程中直接變成不可用狀態。


踩過的坑總結

這次實際部署下來,比較值得記一下的主要就是這幾個:

  • ASSETS 是 Cloudflare Pages 的保留 binding 名稱,自己的 D1 / R2 binding 不要用這個名字
  • platformProxy.persist: true 可以讓本地 dev 的 D1 資料持續存在,不需要每次重開都重新處理
  • D1 / R2 的 binding 名稱就是程式裡 platform.env 底下取得服務時使用的名稱
  • platform.env 的型別可以在 src/app.d.ts 裡補上
  • D1 migration 最好直接放進 Git,作為 Schema 的歷史
  • 修改 Schema 後,記得先跑 migration,再部署新程式碼
  • 部署前跑一次 lintcheckbuild,可以少掉很多不必要的 production 問題

最後

整體來說,SvelteKit + Cloudflare Pages 這個組合在個人博客規模下體驗很好:免費、Edge 快、繞過所有伺服器維運。

SvelteKit 負責網站本身,Pages 負責部署與 SSR,D1 處理資料,R2 處理圖片,基本上每個東西都有很明確的用途。

最重要的是不用自己養一台 VPS。

如果只是個人博客,這種架構其實已經足夠了。

這次真正花時間的地方也不是部署本身,而是一些 Cloudflare 特有的 binding、platformProxy 和 D1 migration 細節。這些東西第一次碰的時候比較容易卡住,所以順手把這次的部署過程和踩過的坑記下來。

如果你也正在考慮把 SvelteKit 搬到 Cloudflare,這套可以直接試。整個架構不複雜,個人博客的規模也很適合。