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"]}'
// 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" }]
}