PetaCMS
· MOVON 佐藤
#CMS#Cloudflare#Edge Computing#設計

既存サイトを壊さない CMS の設計 — Cloudflare Edge で作った PetaCMS

WordPress リニューアルせずに更新機能を追加したい。そんな需要から生まれた PetaCMS が、なぜ Cloudflare Workers + Shadow DOM + Custom Hostnames という構成を選んだのか。

なぜ「既存サイトに後付け」なのか

Web サイトを持つ企業の多くは、更新したいだけなのにリニューアルを迫られるという不合理な状況に置かれている。

  • ABOUT は去年のまま
  • 営業時間が変わったのにお知らせを更新できない
  • ブログを始めたいが既存サイトに合うテンプレが無い

解決策として「WordPress 導入 → サイト全体を WP で作り直し」というのが常道だったが、これはサイト全体を壊す解決策だ。数ヶ月の制作時間、SEO 順位リセット、デザインやり直し…。

PetaCMS はこれに対して**「既存サイトに script タグを 1 行追加するだけで CMS 機能が付く」**というアプローチを取った。既存の HTML/CSS/PHP は一切触らない。

技術的チャレンジ

この要件は技術的には結構厄介だ。

1. 既存サイトの CSS を破壊しない

HTML に widget を埋め込むと、その widget の CSS が既存サイトと衝突する可能性がある。a { color: red } なんて書いたら、既存サイト全リンクが赤くなる。

解決: Shadow DOM (open mode) を使って完全に CSS scope を分離。

const host = document.querySelector('[data-petacms-posts]');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `<style>/* ここは独立空間 */</style>...`;

2. 既存サイトの CSP (Content-Security-Policy) と共存

まともなサイトは CSP ヘッダを設定している。widget が script-srcconnect-src に弾かれるとロードできない。

解決: CDN ドメイン (cdn.peta-cms.com) と API ドメイン (cdn.peta-cms.com) を単一 origin に統一し、顧客には「https://cdn.peta-cms.comscript-srcconnect-src に追加」という最小の CSP 拡張だけを依頼。

3. SaaS テナント分離

複数顧客の記事データが混ざらない保証が必要。かつ、1000 サイト x 100 記事のスケールで p95 < 50ms を維持したい。

解決: Cloudflare D1 を使い、「共有 DB + site_id 行レベル分離」 というモデルを採用。

// tenantDb(siteId) ラッパー経由以外の DB アクセスは禁止
const db = tenantDb(env.DB, siteId);
await db.select().from(posts).where(eq(posts.siteId, siteId));

全 CRUD に site_id = ? が強制されるよう、linter とランタイムガードで二重に保護している。

4. 顧客独自ドメインでの記事配信

https://blog.example.com/news/123 で PetaCMS 管理の記事を SSR 配信したい。これが意外と厄介で、SSL 証明書・ホスト認証・DNS verify など全部必要。

解決: Cloudflare for SaaS で顧客ドメインを自動 verify 。

// /sites/:siteId/hostnames エンドポイントで CF API を叩く
await fetch('https://api.cloudflare.com/client/v4/zones/.../custom_hostnames', {
  method: 'POST',
  body: JSON.stringify({ hostname: 'blog.example.com', ssl: { method: 'http' } }),
});

5. キャッシュ無効化

記事を更新したら、既存サイトに埋め込まれた widget がすぐ新しい内容を表示すべき。でも CDN キャッシュが古い JSON を配り続けたら意味ない。

解決: sites.rev フィールドを更新毎に bump し、manifest.json 経由で最新 rev を配布。 widget は manifest を見て必要に応じて再取得する。

// 記事更新時
await bumpSiteRev(siteId);
await writeManifest(siteId, { rev: newRev });
// → CDN は Cache-Control で短時間キャッシュ、manifest は常に最新

なぜ Cloudflare 完結か

この構成の中心には「Cloudflare エコシステム内で完結させる」という思想がある。

  • Workers: API / SSR / CDN エッジロジック
  • D1: SQLite ベースの DB、Workers から低レイテンシ
  • KV: host → site_id 解決テーブル、manifest
  • R2: 画像ストレージ、egress 無料
  • Queues: 予約公開 / 画像最適化の非同期処理
  • Pages: 管理画面 SPA
  • for SaaS: 顧客ドメイン証明書自動発行

これらは相互に組み合わせが最適化されており、月額数ドル〜数十ドルで 1000 テナントを捌ける。Vercel / AWS + Next.js の一般的な構成だと、D1 や for SaaS の代替を外部サービスで組む必要があり、コストが跳ね上がる。

個人事業で開発する SaaS にとって、この**「1人で運用できる」基盤コスト**は死活的に重要だ。

得られた結果

  • 管理画面: Cloudflare Pages で月 $0(無料枠)
  • API: Workers 標準プランで月 $5
  • D1 / R2 / KV: ほぼ無料枠内
  • ペネトレーションテスト 15/15 通過
  • Lighthouse: LP 99/100/100/100、docs 100/95/100/100

まとめ

「既存サイトを壊さない CMS」という要件は、Cloudflare の各プリミティブを素直に組み合わせることで、1人開発でも実現可能な構成に収まった。

  • Shadow DOM で CSS 隔離
  • D1 + site_id でテナント分離
  • KV + manifest でキャッシュ制御
  • for SaaS で独自ドメイン

「CMS = Headless か WordPress か」という二択ではなく、「既存サイトにペタッと貼る」という第三の選択肢が、小さなチームでも作れる時代になった。


PetaCMS を試したい方は 14 日無料トライアル(クレカ不要)からどうぞ。登録直後にサンドボックスサイトで全機能を試せます。