Cloudflare for SaaS + Workers for Platforms demo — powered by Hono
Architecture
Domain-specific Content (D1) — ドメインに応じたデータを Workers + D1 から取得
皇居、国会議事堂、秋葉原。日本の政治と文化の中心地。
池袋を中心とした商業エリア。サンシャインシティや雑司が谷の鬼子母神。
都内最大の人口を誇る住宅地。下北沢や三軒茶屋など個性的な街が点在。
Pages
Presentation
Source Code
import { Hono } from "hono"; // ホスト名 → テナント設定のマッピング const TENANTS: Record<string, TenantConfig> = { "cffs.norimiso.net": { workerName: "cffs-tenant-norimiso", r2Prefix: "norimiso" }, "cffs.nori-labo.com": { workerName: "cffs-tenant-norilabo", r2Prefix: "norilabo" }, }; const app = new Hono<{ Bindings: Env; Variables: Variables }>(); // テナント解決ミドルウェア app.use("*", async (c, next) => { const tenant = TENANTS[new URL(c.req.url).hostname]; c.set("tenant", tenant); await next(); }); // / → D1 からドメイン別データを取得して動的 HTML 生成 (キャッシュ付き) app.get("/", async (c) => { /* ... */ }); // /content.html → R2 バケットからテナント別コンテンツを配信 (キャッシュ付き) app.get("/content.html", serveR2Content); app.get("/content/*", serveR2Content); // パージ API (Bearer 認証) app.post("/api/purge", async (c) => { /* ... */ }); // その他 → Workers for Platforms でテナント Worker を呼び出し app.all("*", async (c) => { const worker = c.env.DISPATCHER.get(tenant.workerName); return worker.fetch(c.req.raw); }); export default app;
// Workers Cache API でエッジにレスポンスをキャッシュ // テナント別の Cache-Tag でタグベースのパージが可能 (Enterprise) // キャッシュ読み取り const cache = caches.default; const cached = await cache.match(new Request(url)); if (cached) return cached; // X-Cache: HIT // キャッシュ書き込み (Cache-Control で TTL、Cache-Tag でタグ制御) const response = new Response(body, { headers: { "Cache-Control": "public, max-age=3600", "Cache-Tag": `tenant:${tenant.r2Prefix},type:r2`, }, }); c.executionCtx.waitUntil(cache.put(cacheKey, response.clone())); // Cloudflare for SaaS ではキャッシュは SaaS プロバイダーのゾーンで管理 // → cffs.nori-labo.com のリクエストも norimiso.net ゾーンのキャッシュに保存
// Bearer 認証付きパージエンドポイント app.post("/api/purge", async (c) => { const auth = c.req.header("Authorization"); if (auth !== `Bearer ${c.env.PURGE_TOKEN}`) return c.json({ error: "Unauthorized" }, 401); const { paths, hosts } = await c.req.json(); const cache = caches.default; for (const host of hosts) { for (const path of paths) { await cache.delete(new Request(`https://${host}${path}`)); } } return c.json({ purged: paths.length * hosts.length }); }); // Cache Tags でテナント単位パージ (Enterprise Zone): // curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \ // -H "Authorization: Bearer {token}" -d '{"tags":["tenant:norimiso"]}'
// R2 から取得した画像を Images binding で最適化 // Accept ヘッダで WebP/AVIF を自動判定、Vary: Accept でフォーマット別キャッシュ const obj = await env.R2_CONTENT.get("norimiso/hero.jpg"); // Accept ヘッダでブラウザ対応フォーマットを判定 const accept = request.headers.get("Accept"); let format = "image/jpeg"; if (/image\/avif/.test(accept)) format = "image/avif"; else if (/image\/webp/.test(accept)) format = "image/webp"; // Images binding で変換 (リサイズ + フォーマット変換 + 品質調整) const optimized = await env.IMAGES .input(obj.body) .transform({ width: 1200, fit: "scale-down" }) .output({ format, quality: 85 }); // Cache-Tag: tenant:norimiso,type:image + Vary: Accept でキャッシュ
// Tenant Worker: テナント固有の Static Assets と API を提供 // Workers for Platforms の Dispatch Namespace にデプロイされる export default { async fetch(request, env) { const url = new URL(request.url); // /api/info → テナント情報を JSON で返却 if (url.pathname === "/api/info") { return Response.json({ worker: "cffs-tenant-norimiso", hostname: url.hostname, message: "This response is from the norimiso.net tenant worker", }); } // それ以外 → Static Assets (public/index.html) を返却 return env.ASSETS.fetch(request); }, };
CREATE TABLE locations ( id INTEGER PRIMARY KEY AUTOINCREMENT, domain TEXT NOT NULL, -- ホスト名でフィルタ prefecture TEXT NOT NULL, -- 都道府県 ward TEXT NOT NULL, -- 区名 description TEXT NOT NULL -- 説明文 ); -- norimiso.net → 東京都の区、nori-labo.com → 大阪府の区 INSERT INTO locations (domain, prefecture, ward, description) VALUES ('cffs.norimiso.net', '東京都', '渋谷区', 'スクランブル交差点で知られる...'), ('cffs.nori-labo.com', '大阪府', '北区', '梅田を擁する大阪の玄関口...'); -- Dispatcher が domain でフィルタして 3 件ランダム取得: -- SELECT * FROM locations WHERE domain = ? ORDER BY RANDOM() LIMIT 3
{
"name": "cffs-dispatcher",
// Workers for Platforms の Dispatch Namespace バインディング
"dispatch_namespaces": [{ "binding": "DISPATCHER", "namespace": "cffs-production" }],
// Content API Worker への Service Binding
"services": [{ "binding": "CMS", "service": "cffs-contents" }],
// テナント別コンテンツ用 R2 バケット
"r2_buckets": [{ "binding": "R2_CONTENT", "bucket_name": "cffs" }],
// Images binding (WebP/AVIF 自動変換)
"images": { "binding": "IMAGES" }
}