這篇是這個網站自身的部署紀錄。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,再部署新程式碼
- 部署前跑一次
lint、check、build,可以少掉很多不必要的 production 問題
最後
整體來說,SvelteKit + Cloudflare Pages 這個組合在個人博客規模下體驗很好:免費、Edge 快、繞過所有伺服器維運。
SvelteKit 負責網站本身,Pages 負責部署與 SSR,D1 處理資料,R2 處理圖片,基本上每個東西都有很明確的用途。
最重要的是不用自己養一台 VPS。
如果只是個人博客,這種架構其實已經足夠了。
這次真正花時間的地方也不是部署本身,而是一些 Cloudflare 特有的 binding、platformProxy 和 D1 migration 細節。這些東西第一次碰的時候比較容易卡住,所以順手把這次的部署過程和踩過的坑記下來。
如果你也正在考慮把 SvelteKit 搬到 Cloudflare,這套可以直接試。整個架構不複雜,個人博客的規模也很適合。
評論
…