MarkdownからCloudflareへ——このブログを作った理由と仕組み
Astro、Cloudflare Workers、R2、D1を組み合わせ、Markdownを正本にした個人ブログの設計思想・独自実装・公開フローを実画面とともに紹介します。

ブログを作った。
ただ記事を表示するだけなら、既製のサービスを使うほうが速い。それでも自分で作ったのは、文章と画像を自分の手元に残しながら、書くことと公開することの距離を短くしたかったからだ。
この記事では、完成した画面だけでなく、なぜこの構成にしたのか、どこを独自実装したのか、一般的なAstroブログと何が違うのかまでまとめる。
先に全体像
このブログは、ローカルのMarkdownと画像を正本にする。
実行するコマンドは一つだけだ。
npm run publish
内部では、検証、画像同期、データベース反映、ビルド、デプロイまでを順番に行う。
背景——管理画面ではなく、ファイルとして書きたい
既存のブログサービスには、エディタ、画像管理、公開ボタン、分析機能まで揃っている。一方で、自分が欲しかった執筆環境はもっと単純だった。
- 普段使っているエディタでMarkdownを書きたい
- 記事と画像をGitで履歴管理したい
- サービス固有の形式に本文を閉じ込めたくない
- データベースやストレージを失っても再構築できるようにしたい
- 表示速度と運用コストは抑えたい
公開環境を正本にしない。手元のMarkdownから、公開環境を何度でも作り直せるようにする。
これが設計の中心にある。
目的
目指したのは「機能が多いCMS」ではなく、次の条件を満たす小さな出版システムだった。
- 執筆はMarkdownだけで完結する
- 公開操作は一つのコマンドにまとめる
- 静的配信を基本にして高速に表示する
- 画像は長期キャッシュできるURLで配信する
- 将来、ホスティング先を変更しても記事を持ち出せる
管理画面、認証、オンライン編集、コメント機能は最初から作らないと決めた。必要になるか分からないものを先に持つと、書くためのシステムが、システムを保守するためのシステムになってしまう。
技術スタック
| レイヤー | 採用技術 | 役割 |
|---|---|---|
| UI / SSG | Astro 7 | Markdownの読み込み、静的HTML生成、画像最適化 |
| 配信 | Cloudflare Workers + Static Assets | HTML・CSS・フォントの配信、R2画像のルーティング |
| 画像 | Cloudflare R2 | 公開用画像オブジェクト |
| データ | Cloudflare D1 | 記事、タグ、画像メタデータと関連 |
| 執筆 | Markdown + Astro Content Collections | frontmatterの型検証と本文管理 |
| 自動化 | Node.js + Wrangler | 検証、同期、ビルド、デプロイ |
| 品質 | Node Test Runner | コンテンツ境界、生成HTML、性能予算のテスト |
Astroを選んだ理由
記事ページは、アクセスのたびにサーバーで組み立てる必要がない。AstroならMarkdownから静的HTMLを生成でき、クライアントへ送るJavaScriptも必要な場所だけに限定できる。
Content Collectionsのschemaで、slugやタグの形式、日付、画像参照をビルド時に検証できる点も大きい。
z.object({
title: z.string(),
slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
publishedAt: z.coerce.date(),
draft: z.boolean().default(false),
tags: z.array(
z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)
),
heroImage: z.optional(image()),
});
Cloudflareを選んだ理由
静的アセット、Worker、オブジェクトストレージ、SQLデータベースを同じ基盤上で扱える。現在の規模では大掛かりなサーバーを持つ必要がなく、配信経路も短い。
通常のページはStatic Assetsから返し、/images/*だけをWorkerがR2へ解決する。
if (url.pathname.startsWith('/images/')) {
const object = await env.BLOG_ASSETS.get(key);
return new Response(object.body, { headers });
}
return env.ASSETS.fetch(request);
独自実装したところ
スターターを見た目だけ変更したのではなく、公開までの経路をブログ向けに組み直している。
1. Markdownを変更しない画像パイプライン
記事から画像を参照するときは、普通の相対パスを書く。

publish時に画像のSHA-256を計算し、次の形式でR2へ保存する。
images/{post-slug}/{sha256}.{ext}
HTMLを生成するときだけ公開URLへ置換するため、MarkdownにCloudflare固有のURLは残らない。画像を変更すればhashも変わり、古いキャッシュとの衝突を避けられる。
2. 再構築できるD1
D1には記事本文を変換したHTML、タグ、画像メタデータ、関連情報を保存する。ただし、D1を唯一の原本にはしない。
Markdownと元画像がrepositoryに残っているため、D1とR2は再生成できる。バックアップ戦略を複雑にする代わりに、派生データを作り直せる設計にした。
3. 公開前レビューと境界テスト
publish時には、単にアップロードするだけでなく、次の問題を検出する。
- slugやタグの不正な形式
- slugの重複
- 存在しないローカル画像
- descriptionの不足や過不足
- 似たタグ、似た記事
- 壊れた内部リンク
- HTMLサイズとビルド時間の性能予算
エラーにすべき問題と、確認を促すレビューを分けている。descriptionが短いだけで公開を止める必要はないが、気づかず公開するのも避けたいからだ。
4. URLだけで展開するリンクカード
Markdown内にURLを単独行で置くと、remark pluginがリンクカードへ変換する。
内部記事ではローカルのfrontmatterと画像を読むため、ネットワーク接続なしでもビルドできる。

外部URLではOGメタデータを読み、取得できない場合にも通常リンクへ安全にフォールバックする。

5. SNS共有用画像
記事のhero imageから1200×630のJPEGを生成し、Open GraphとX Cardへ設定する。og:imageだけでなく、MIME type、幅、高さ、代替テキストも明示した。
これにより、ブラウザ上の画像形式に依存せず、SNSクローラーが扱いやすいカードを生成できる。
6. Light / Dark theme
テーマは次の優先順位で決める。
- ユーザーが以前選んだテーマ(
localStorage) - OS/ブラウザの
prefers-color-scheme - OSが指定しない場合はlight
初期描画前に<html>へテーマを設定し、ページを開いた瞬間に色が反転するちらつきを抑えている。
一般的なAstro Blog Starterとの違い
| 項目 | 一般的なスターター | このブログ |
|---|---|---|
| 記事の正本 | Markdown | Markdown |
| 画像 | ビルド成果物へ同梱 | 元画像はrepo、公開画像はhash付きでR2 |
| メタデータ | frontmatterのみ | frontmatterからD1も再構築 |
| 公開 | Astro build | validate → review → R2 → D1 → build → deploy |
| 検索 | 未実装または外部サービス | 静的HTML上のクライアント検索 |
| リンク | 通常のアンカー | 内部・外部リンクカードをremarkで生成 |
| SNS画像 | 固定画像または元画像 | 記事ごとに1200×630 JPEG生成 |
| テーマ | 固定 | OS連動+選択保存 |
| 品質保証 | ビルド確認中心 | コンテンツ、リンク、HTML、性能を自動テスト |
違いは、D1やR2を使っていること自体ではない。ローカルの原稿を中心に、公開用データを一方向に生成する境界を明確にしたことが一番の違いだ。
実際にできた画面
Homeは、説明、最新記事、件数、タグを一画面で把握できるBento型にした。記事数が少ない段階でも不自然な空白や重複が出ないよう、要素数に応じてレイアウトを変えている。

Blog、Snaps、Searchは同じグリッド寸法を共有しつつ、中身の優先順位を変えた。
- Blogはタイトル、説明、タグを重視
- Snapsは画像を入口にする
- Searchは入力結果と一致件数を即時更新
- Tagsは分類と記事数を一覧化
ダークモードでも、単純な白黒反転ではなく、背景、カード、境界線、補助文字のコントラストを変数で管理する。

公開フロー
npm run publishの中では、次の順序を崩さない。
1. frontmatterと参照ファイルをvalidate
2. 公開前レビューを表示
3. 画像hashを計算してR2へupload
4. MarkdownをHTMLへ変換
5. D1へposts / tags / assetsを反映
6. Astroで静的ページをbuild
7. WranglerでWorkerとassetsをdeploy
途中で失敗した場合、後続処理は実行しない。同じ入力でもう一度実行できるよう、同期処理は冪等にしている。
作って分かったこと
「便利な管理画面がない」は欠点とは限らない
一人で使うブログなら、Git、エディタ、Markdownがすでに管理画面の役割を果たしている。認証やフォームを追加しないことで、攻撃面と保守対象を減らせた。
データベースを使っても、データベース中心にしなくてよい
D1は一覧や関連を扱うには便利だが、文章の編集履歴はGitのほうが自然だ。役割ごとに正本を決めれば、両者を無理なく併用できる。
小さなブログにもテストは効く
記事が増えるほど、壊れた画像、slugの重複、タグの表記揺れは人間の確認だけでは漏れる。表示のテストだけでなく、コンテンツそのものの境界をテストする価値がある。
これから
現時点で、書く・探す・共有する・公開するための基本機能は揃った。次に追加するなら、機能数を増やすより、次の部分を育てたい。
- 記事が増えたときの検索インデックス
- 画像ごとの適切なresponsive size
- アクセシビリティの継続的な自動検査
- publish処理の差分同期
- 実記事を増やしながら情報設計を調整
このブログ自体が、完成品であると同時に実験場所でもある。仕組みを複雑にしすぎず、実際に書く中で必要になったものだけを追加していく。