この記事の目次
タップすると同じページ内の見出しへ移動します
8 sections 01 この記事でわかること ↓ 02 全体構成 ↓ 03 なぜD1なのか ↓ 04 なぜR2 + Images bindingなのか ↓ 05 Next.jsをWorkersで動かす選択肢 ↓ 06 環境変数の二重構造に注意 ↓ 07 コスト ↓ 08 まとめ ↓ この記事でわかること
このブログは Cloudflare Workers 上で動いています。記事データは D1(SQLite)、画像は R2、フレームワークは Next.js 互換の vinext。この構成を選んだ理由と、実際に運用して分かった注意点を、全部そのまま書きます。
「個人ブログをサーバーレスで作りたい」「VPSの面倒を見たくない」人に向けた実例ベースの解説です。
全体構成
txt ⧉
ブラウザ │ ▼ Cloudflare Workers(1プロセスでSSR + API) ├─ ASSETS(静的ファイル配信) ├─ D1(記事・コメント・設定をSQLiteで保存) ├─ R2(サムネイル画像) ├─ Images binding(画像のリサイズ・変換) └─ send_email binding(管理者OTPメール)
ポイントは「全部が1つのWorkerに同居」していることです。SSRもAPIも画像配信も同じエントリポイントから処理されるので、サービス間通信もCORSも考えなくてよくなります。
なぜD1なのか
個人ブログのデータは「読みがほぼ全て、書きはたまに」です。D1(SQLite)はこの用途にちょうどよく、しかも無料枠が大きい。
実際のテーブル設計はシンプルです:
sql ⧉
CREATE TABLE posts ( id TEXT PRIMARY KEY, slug TEXT UNIQUE NOT NULL, title TEXT NOT NULL, excerpt TEXT NOT NULL, content TEXT NOT NULL, tags TEXT NOT NULL, -- JSON文字列として保存 status TEXT NOT NULL, -- draft / published likes INTEGER DEFAULT 0, views INTEGER DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL );
タグを正規化せずJSON文字列で持つのは手抜きに見えますが、記事数が数百件規模のブログではこれで十分です。タグ集計が重くなったら SELECT tags FROM posts WHERE status='published' だけ読んでアプリ側で集計すれば足ります(contentカラムを読まないのがコツ)。
注意点: D1には接続プールも長いトランザクションもありません。「読んでから書く」間に別リクエストが割り込む競合は普通に起きるので、更新系は INSERT ... ON CONFLICT DO UPDATE や UPDATE ... WHERE updated_at = ?(楽観ロック)で書くのが安全です。
なぜR2 + Images bindingなのか
サムネイルはR2に置き、配信時にImages bindingでリサイズします。アップロード時にはブラウザ側でも圧縮します:
ts ⧉
const bitmap = await createImageBitmap(file); const canvas = document.createElement("canvas"); // 最大幅1600pxに縮小してWebP 0.82で再エンコード const blob = await new Promise((r) => canvas.toBlob(r, "image/webp", 0.82));
「サーバーでやればいいのでは?」と思いがちですが、WorkersのCPU時間は貴重なので、クライアントで済む処理はクライアントでやるのがコスト的に正解でした。
Next.jsをWorkersで動かす選択肢
本家Next.jsをWorkersで動かすにはいくつか選択肢がありますが、このブログは vinext(ViteベースのNext.js互換ランタイム)を使っています。App Router、Server Components、 next/image 相当の画像最適化まで動きます。
ただし注意: この手の互換レイヤーは「対応するNext.jsのバージョン」が厳密に決まっています。package.jsonでは ^ を付けず exact pin して、ランタイムとペアで更新する運用にしています。
環境変数の二重構造に注意
Workersでは環境変数が「ビルド時(process.env)」と「実行時(envバインディング)」の2系統になります。NEXT_PUBLIC_* はビルド時にクライアントコードへ埋め込まれますが、Workerの実行時にしか値がないものは両方を見るヘルパーが必要です:
ts ⧉
export async function getRuntimeValue(key: string) { const runtime = getCloudflareEnv()?.[key]; if (typeof runtime === "string" && runtime.length > 0) return runtime; const nodeValue = process.env[key]; return nodeValue && nodeValue.length > 0 ? nodeValue : undefined; }
これを用意しておかないと、「ローカルでは動くのに本番では環境変数が空」という謎のトラブルを踏みます(踏みました)。
コスト
この構成の月額は、個人ブログのトラフィックなら実質0円です。Workers無料枠(リクエスト/日 10万)、D1無料枠(5GB)、R2無料枠(10GB)に収まります。独自ドメイン代だけが実費です。
まとめ
• 1 WorkerにSSR/API/画像を同居させると構成が劇的にシンプルになる
• D1は個人ブログに十分。ただし楽観ロックは自前で入れる
• 画像圧縮はクライアント、配信変換はImages binding
• 環境変数はビルド時/実行時の二重構造を吸収するヘルパーを最初に書く
次回はこの構成の上でD1のFTS5を使った日本語全文検索を解説します。
Turnstile site key が未設定のため、このフォームは送信できません。管理者は NEXT_PUBLIC_TURNSTILE_SITE_KEY を設定してください。