

Astro + Cloudflareでこのブログを作った
この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。
記念すべき最初の記事は、このブログ自体をどう作ったかにした。ベタだけど、構築直後の記憶が一番新しいうちに残しておきたい。あと、同じことをやろうとしている人の地雷除去にもなると思うので。
何が欲しかったのか
要件はシンプルだった。
- 無料で公開できる
- メンテナンスフリー(サーバーの面倒を見たくない)
- いずれ広告を載せて収益化したい(儲けが目的ではなく、運用費を賄えたらいいな程度)
この3つを満たすなら、答えはだいたい決まっていて、静的サイト(SSG)一択になる。DBもサーバーもないから、そもそも壊れる場所がない。これがメンテフリーの本質だと思う。
技術選定でちょっと寄り道した
EmDashが気になった、けど見送った
最初、Cloudflareが出しているAstroベースのCMS「EmDash」が気になっていた。が、調べたらまだプレビュー段階で、しかもサーバーを動かしてエッジコンピュートに課金して……という構成だった。「メンテフリーが欲しい」と言っている人間が選ぶものじゃない。素直に見送り。触ってみたい欲だけはまだある。
結局:Astro + Cloudflare Workers (Static Assets)
普段からTypeScriptを書いているのでAstroとの相性は良いし、Markdownで記事を書いてgit pushするだけで公開できる。ホスティングはCloudflare。これで決まり。
作る
ここはあっけないくらい簡単だった。
npm create astro@latest # Blogテンプレート、TypeScript Strictcd my-blognpm run devhttp://localhost:4321 を開いたら、もうブログの雛形が表示されている。あとはGitHubにリポジトリを作ってpush。ここまでは何も詰まらなかった。
問題はこの先で起きた。
ハマり①:画像が表示されない(/_image が404)
Cloudflareにデプロイして公開URLを開いたら、画像だけ表示されない。F12のNetworkタブを見ると、こんなリクエストが404になっていた。
/_image?href=%2F_astro%2Fblog-placeholder-2.xxxx.jpg&w=720&h=360&f=webpローカルの npm run dev では普通に出ていたのに、本番だけ出ない。
原因
/_image は、Astroがdev時やSSR時に画像を実行時変換するためのエンドポイント。逆に、静的ビルドのときは <Image> がビルド時に画像を最適化して dist/_astro に吐き出すので、本来このエンドポイントは登場しない。
つまり、HTMLに /_image?... が残っているということは、ビルド時最適化が効かず、実行時エンドポイント前提のURLが焼き込まれている状態だった。
犯人はデプロイ方法。最初、プロジェクトがSSR Workerとしてデプロイされていた(公開URLが .workers.dev だったのがヒント)。Workersはオンデマンド実行が前提の世界なので、静的最適化済みの画像とは噛み合っていなかった。
切り分け
ビルドが悪いのか配信が悪いのかを、手元で分けて確認した。
npm run buildgrep -rl "/_image" dist/ # → 何も返らない=HTMLに/_imageは無いnpm run preview # → 画像が表示されるdist のHTMLに /_image は無く、preview(本番ビルドの静的配信)では画像が出た。これで「ビルドは正しい、配信側が悪い」と確定。
解決
純粋な静的配信になるように、リポジトリ直下に wrangler.jsonc を置いた。
{ "name": "my-blog", "compatibility_date": "2026-06-26", "assets": { "directory": "./dist", "not_found_handling": "404-page" }}ポイントは、main も binding も書かないこと。Workerスクリプトを持たせず、静的アセットだけを配信する形にするのがコツだった。これで dist/_astro の最適化済み画像が /_astro/xxx.webp の直リンクで配信され、無事に表示された。
ちなみにAstroの設定(output: static がデフォルト、アダプタ無し、sharpあり)は最初から正しかったので、astro.config 側は一切いじっていない。完全に配信の問題だった。
ハマり②:Cloudflareの「Pages」が見つからない
「じゃあ普通のCloudflare Pagesで静的配信すればよかったのでは」と思って作り直そうとしたら、Pagesの新規作成の入口が見当たらない。「Create application」を押してもWorker作成のウィザードが立ち上がる。
これは自分のミスではなく、CloudflareがPagesをWorkersに統合する方向に舵を切っているためだった。今は新規作成がWorkers寄りになっていて、Free/ProプランだとサブドメインだけをCloudflareゾーンにする「subdomain setup」も使えない(あれはEnterprise限定)。
なので無理にPagesを探すのはやめて、Workers + Static Assetsで素直に組む方針に切り替えた。将来的にもCloudflareはこっちに寄せていくので、結果的に筋の良い選択だったと思っている。
ドメイン戦略:親を載せるか問題
blog.hoge.dev というサブドメインで公開したかった。Workersのカスタムドメインは「そのドメインのDNSをCloudflareが管理していること」が条件になる。
最初は「親の hoge.dev のネームサーバーは動かしたくない、blog だけCloudflareに委任したい」と思っていた。が、前述の通りそれはEnterprise限定。
そこで考え方を変えた。自分はこれから hoge.dev 配下に自分用のツールをいくつも作っていきたい。tool.hoge.dev、api.hoge.dev みたいに。だったら、hoge.dev を常設の実験場ドメインとして、親ごとCloudflareに載せてしまうほうが圧倒的に取り回しがいい。
- サブドメインを好きなだけ生やせる
- 1サブドメイン = 1プロジェクト(Worker / Pages)を自由に割り当てられる
- DNSレコードも証明書もCloudflare内で自動完結
- 自分用ツールにはCloudflare Accessで認証の壁を無料で付けられる
「親NSを動かす」のは所有権の移管ではなく、DNSの管理元をCloudflareにするだけ。レジストラはそのまま。失うものは実質なかった。
ネームサーバーを切り替えて、DNSの**伝播(でんぱ)**を待ち(.dev は比較的速い)、Activeになったところでカスタムドメインを追加。blog.hoge.dev で無事に公開できた。
余談:「伝播」をずっと「でんばん」だと思っていた。正しくは「でんぱ」。電波と同じ音。気づけてよかった。
ドメインを繋いだら astro.config の site を実ドメインに直すのも忘れずに。RSSやsitemap、canonical URLがこれを基準に生成されるので。
site: 'https://blog.hoge.dev',おまけ:URLパスを整える(任意)
これは完全に好みの問題なので、気にならない人は読み飛ばしていい。
Astroのblogテンプレートは、デフォルトだと記事のURLが /blog/記事名/ になる。普通のドメインならこれで何も問題ない。でも自分の場合、ドメイン自体が blog.hoge.dev なので、そのままだと blog.hoge.dev/blog/記事名/ になって「blog」が二重になる。ちょっとだけ気持ち悪い。
なので /blog/ を /articles/ に変えた。やることはシンプルで、ルーティングを担っている src/pages/blog/ ディレクトリをリネームするだけ。
git mv src/pages/blog src/pages/articlesここで一つ注意。記事の実体が入っている src/content/blog/(コレクション名)はURLとは無関係なので、触らない。[...slug].astro の中の getCollection('blog') の 'blog' もコレクション名なので、そのまま。変えるのは src/pages/ 側のディレクトリだけ。
あとは内部リンクとRSSの /blog/ を置換して、取りこぼしがないか確認すれば終わり。
grep -rn "/blog" src/ # 内部リンク・rss.xml.js などを洗い出して置換npm run buildgrep -rn "/blog" dist/ # distに /blog が残っていなければ漏れなし繰り返すけど、ここは完全にオプション。サブドメインを blog. にしている人の、見た目のこだわりみたいなものです。
最終的な構成
flowchart TD A["Markdown (GitHub管理)"] -->|git push| B["Astro(静的ビルド / dist)"] B -->|wrangler.jsonc で静的アセット配信| C["Cloudflare Workers (Static Assets)"] C -->|Custom Domain| D["blog.hoge.dev(TLS自動)"] classDef source fill:#E8EAF6,stroke:#3F51B5,color:#1A237E classDef build fill:#E3F2FD,stroke:#1976D2,color:#0D47A1 classDef host fill:#E8F5E9,stroke:#388E3C,color:#1B5E20 classDef publish fill:#FFF3E0,stroke:#F57C00,color:#E65100 class A source class B build class C host class D publish
CMSは入れていない。記事はリポジトリの .md をそのまま管理して、エディタはVS Code + Astro公式拡張。Content CollectionsのZodスキーマからfrontmatterが補完・検証されるので、TypeScriptと同じ「型に守られながら書く」感覚で記事も書ける。これが個人的にはすごく快適。
これからやること
- 記事を書く(AdSense審査の前提でもある)
- Google Analytics(partytown経由で導入予定)
- AdSense(記事が溜まってから)
- Cookie同意まわり(EEA向けに出すなら)
とりあえず、無料でメンテフリーな自分のブログが自分のドメインで公開できた。満足。次は中身を埋めていく番。