AstroをSSGからSSRへ。Cloudflare Workers+R2で複数サイト対応の独自CMSを自作
これまでは、Macのローカル環境でAstro本体と記事データを管理し、記事を作成するたびにターミナルからビルドしてサイトを更新していました。
特に問題なく運用できていましたが、記事数が増えてくると記事データそのものが大きくなってきます。また、記事を追加・修正するたびにMacから作業する必要があるため、出先や別の環境からでも記事を更新できるようにしたいと考えるようになりました。
そこで今回、サイト本体と記事データを分離し、ブラウザから記事を作成・編集・公開できる独自CMSを作ることにしました。
Astro本体はGitHubで管理し、GitHubとCloudflareを連携しています。サイトのデザイン変更や機能追加などは、GitHub側からAstro本体を更新できます。
一方、記事データは大量にあるため、GitHubには置かずCloudflare R2に保存することにしました。
記事数が少なければGitHubだけで管理する方法でも十分ですが、今回は記事数が非常に多いため、サイト本体と記事データを分離する構成にしています。
さらに、記事を追加するたびにAstro本体をビルドする必要がないよう、これまでのSSGからSSRへ変更しました。
今回の構成
現在は、以下のような構成になっています。
- サイト本体:Astro(SSR)
- 実行環境:Cloudflare Workers
- サイト本体の管理:GitHub
- 記事データ:Cloudflare R2
- CMS:Cloudflare Workers上に別途構築
- CMSとR2:Workersのバインディングで接続
また、このCMSは1サイト専用ではありません。
個人で運営している3サイトを、1つのCMSから管理できるようにしています。CMS上で投稿先のサイトを選択し、そのサイトの記事を作成・編集できる仕組みです。
複数のサイトをそれぞれ別々に管理するのではなく、記事管理の部分を1つのCMSにまとめることで、更新作業をできるだけシンプルにしています。
CMSは記事作成に特化
CMSの役割は、記事の作成・編集・管理です。
CMSから記事を作成すると、Markdown(.md)ファイルとしてR2へ保存します。同時に、記事一覧などで使用するJSONにも必要な情報を追加します。
記事そのものはR2に保存し、サイト側では必要なデータを取得して表示する構成です。
これによって、Astro本体と記事データを切り離しています。
SSGからSSRに変更した理由
今回の構成で大きく変えたのが、AstroをSSGからSSRへ変更したことです。
これまでは、記事を追加・変更するたびにAstro本体をビルドしてサイトを更新していました。
しかし、記事数が増えてくると、記事を1件追加するだけでもサイト全体のビルドが必要になります。
そこで、記事データをAstro本体から切り離し、R2に保存されたデータをSSRで利用する構成に変更しました。
これなら、記事を追加するためにAstro本体を毎回ビルドする必要がありません。
さらに、今回はCloudflareの無料枠内でできるだけ運用したいという目的もあります。
そのため、アクセスのたびにR2へデータを取りに行くのではなく、キャッシュを利用してR2のClass A/Class B OperationsやWorkersのリクエスト数をできるだけ抑えるようにしています。
記事一覧などで使用するJSONはキャッシュされており、キャッシュが更新されるタイミングで最新のJSONが読み込まれます。
その際に新しく追加された記事があれば、その内容が新着記事などとしてサイトに反映されます。
つまり、
CMSで記事を追加 → R2へ保存 → JSONを更新 → キャッシュ更新時に最新JSONを取得 → 新しい記事をサイトへ反映
という流れです。
この仕組みによって、記事を追加するたびにサイト全体をビルドする必要がなくなりました。
また、サイトマップやRSSなども更新されるため、Astro本体側で必要になるのは、基本的にデザインの変更や機能追加、メンテナンスなどです。
Cloudflareの無料枠を意識した設計
今回の構成では、できるだけCloudflareの無料枠の範囲内で運用することも意識しています。
ドメインもCloudflareへ移管しているため、できるだけドメイン費用以外の追加コストを発生させずにサイトを維持できる構成を目指しています。
そのため、
- WorkersのCPU Time
- Workersのリクエスト数
- R2のClass A Operations
- R2のClass B Operations
などをできるだけ抑えることを重視しました。
特に記事数が多いため、「必要なデータだけを、必要なタイミングで取得する」ことを基本方針にしています。
D1やKVを使わなかった理由
今回の構成では、Cloudflare D1やKVは使用していません。
D1については、もともとWordPressでデータベースを利用していたため、そこから脱却することも今回の目的の一つでした。
別のデータベースを中心としたCMSにするのではなく、記事をMarkdownとしてR2に保存するシンプルな構成にしています。
KVについては、記事数の多いサイトも含めて運用することを考えると、アクセスが増えた際に無料枠のリクエスト数を消費しやすいため採用しませんでした。
今回はR2に記事データを保存し、キャッシュを利用してアクセス数を抑える構成にしています。
1. サイトマップ/RSSを利用した記事一覧の取得
R2には記事が増えるにつれて、大量のMarkdownファイルが保存されていきます。
これらをR2から1件ずつ取得して記事一覧を作る方法では、記事数が増えるほどアクセス数も増えてしまいます。
そこで、公開サイトにある sitemap.xml や feed.xml を利用して記事一覧を取得する仕組みにしています。
R2内の大量のファイルを直接走査する必要がなくなるため、不要なR2へのアクセスを減らせます。
記事数が増えても、一覧を作るために大量のファイルを読み込む必要がないため、負荷やコストを抑えやすくなります。
2. オンデマンドでの同期
CMSでは、自動的なデータ取得もできるだけ行わないようにしています。
たとえば、カタログJSONなど比較的大きなデータは、CMSを開くたびに取得するのではなく、必要なときにユーザーが「同期」を実行して取得します。
AGENTS.mdで定義している NO_AUTO_FETCH も、この方針に沿ったものです。
必要なときだけデータを取得することで、CMSを開くだけでWorkerが不要な処理を行ったり、R2へアクセスしたりすることを防いでいます。
3. ブラウザ側での事前処理
記事投稿時に必要となるスラッグの生成やFrontmatterの初期値作成などは、可能な限りブラウザ側のJavaScriptで処理しています。
サーバー側で行う処理を必要最小限にすることで、WorkersのCPU Timeを抑えています。
また、ブラウザ側ですぐに処理できるものをWorkerへ送らないことで、投稿時の処理も軽くしています。
4. サイトごとの厳格な分離
3サイトを1つのCMSで管理する以上、別のサイトのデータまで不用意に取得しないことも重要です。
そこで、CMSでは現在操作しているサイトに対応したR2だけを対象にする「Strict Isolation」の考え方を採用しています。
たとえば、3サイトを管理していても、現在操作しているサイトが1つ目のサイトなら、そのサイトに対応したR2だけにアクセスします。
ほかのサイトのデータまで取得したり、全サイトのR2を一度に走査したりすることはありません。
これによって、複数サイトを1つのCMSで管理しながらも、不要な通信や処理が発生しないようにしています。
5. メタデータ取得処理の効率化
記事によっては、記事タイトルなどの情報を取得する際にスラッグがそのまま表示されてしまうケースがありました。
そこで、image:title など複数のメタタグを優先順位に沿ってまとめて解析する方式に変更しました。
必要な情報を少ないリクエストで取得できるようにすることで、不要な再試行や追加処理も減らしています。
まとめ
今回のCMSでは、単に「ブラウザから記事を投稿できるようにする」だけではなく、記事数やサイト数が増えても無駄な処理が増えにくい構成を意識しています。
以前は、Macのローカル環境にAstro本体と記事データをまとめ、記事を追加するたびにサイト全体をビルドしていました。
現在は、
Astro本体 → GitHub
記事データ → R2
記事の作成・管理 → 独自CMS
と役割を分離しています。
そして、AstroをSSR化することで、記事を追加するたびにサイト全体をビルドする必要をなくしました。
CMSから記事を追加するとR2とJSONが更新され、キャッシュが更新されるタイミングで最新のデータがサイトへ反映されます。
さらに、キャッシュやオンデマンド取得、サイトごとのデータ分離などを組み合わせることで、R2のClass A/Class B OperationsやWorkersのリクエスト数をできるだけ抑えています。
3サイトを1つのCMSから管理しながら、記事数が増えても運用コストやサーバー側の処理が大きく増えない。
今回は、そんな構成を目指してCMSとサイト側の仕組みを作り直しました。




