個人サイトの完全なデプロイ記録です。Astro + Supabase + Cloudflare Workers というフルマネージドな構成で、ゼロコストで中英日三言語対応のブログを立ち上げました。記事は二部構成——前半はゼロから同じサイトを作れるデプロイ手順、後半は実際に踏んだ九つの落とし穴を、原因と解決策つきでまとめています。核心は「ゼロ運用・公開すれば即反映・月額 0 円」。
ゼロから公開まで:個人サイト構築の全記録
はじめに
この記事は、このサイト自身の「供述調書」です。ローカルマシン上の数行のコードが、いま皆さんがご覧になっているこのページになるまでの過程を、すべて書き残しました。内容は二部構成です。前半はゼロから始めるデプロイ手順のすべて(このとおりに進めれば、まったく同じサイトが作れます)。後半は実際に踏んだつまずきの記録(どれも私が本当に踏んだもので、原因と解決策まで書いてあります)。
こんな人に向いています。お金をかけずに個人ブログを立ち上げたい人。あるいは未来の自分自身——いつかまた全部壊して作り直したくなったとき、ゼロから手探りしなくて済むように。
完成形:これはどんなサイトか
まずは完成後の姿を見て、全体像をつかんでください。
| 項目 | 現状 |
|---|---|
| フレームワーク | Astro 4(SSR・サーバーサイドレンダリングモード) |
| データベース | Supabase(Postgres。記事/コメント/カテゴリ/サイト設定はすべてここ) |
| ホスティング | Cloudflare Workers(無料枠:1日10万リクエスト、帯域無制限、月3,000ビルド分) |
| ドメイン | 自身で取得。DNSはCloudflare、HTTPSは自動発行 |
| 言語 | 中国語/英語/日本語の3言語を同一ソースで運用 |
| 執筆環境 | 標準搭載の/admin管理画面。ブラウザから直接書いて、そのまま公開 |
| 月額コスト | 0円(ドメインだけが過去の唯一の支出。他はすべて無料枠) |
仕組みを一文で言えばこうです。読者がアクセスするのは常にCloudflareで、CloudflareはリクエストのたびにリアルタイムでSupabaseに記事を問い合わせて返しています。だから管理画面で「公開」を押した瞬間に記事は公開されます。ビルド待ちも再デプロイも不要——この設計でいちばん気に入っている点です。
読者のブラウザ ──► Cloudflare Worker(SSRレンダリング+静的アセット)
│ リクエストごとにリアルタイム照会
▼
Supabase(記事/コメント/カテゴリ/設定)
▲
管理画面 /admin ────────┘ (ブラウザからSupabaseへ直接接続。公開=即反映)
方式の比較:なぜこの構成なのか、どこが物足りないのか
技術選定は長所だけを見て決めるものではありません。主要な選択肢を正直に並べて比較すれば、この構成が自分に合うかどうか判断できるはずです。
主要な選択肢との横比較
| 観点 | 本構成(Astro + Supabase + CF Workers) | WordPress | Next.js + Vercel | Hexo / Hugo(純静的) |
|---|---|---|---|---|
| サーバー運用 | 不要。すべてマネージド | 必要(または有料のマネージドを契約) | 不要 | 不要(ただし公開にはローカルビルドが必要) |
| 公開フロー | ブラウザの管理画面から直接公開、即反映 | 管理画面から直接公開 | 同左 | ローカルで執筆 → ビルド → プッシュ |
| ブラウザでの執筆 | ✅ 標準搭載 | ✅ | 別途CMSが必要 | ❌ |
| データベース | Supabase(Postgres、SQLがフルに使える) | MySQL | 構成次第(外部CMSを繋ぐことが多い) | なし(ファイルがコンテンツ) |
| 無料枠 | CF 1日10万リクエスト+Supabase無料枠 | なし(サーバー代が必要) | あり(商用利用に制限) | 静的ホスティングはほぼ無制限 |
| 対象地域でのアクセス速度 | 地域次第。CFのエッジは中国本土など一部地域ではやや遅く、大半の地域では良好 | サーバー次第 | 地域によって大きく変動 | 地域CDNが使えれば最速 |
| エコシステム/情報量 | Astroは比較的小規模。日本語情報は少なめ | 最大。プラグインもテーマも豊富 | Next.jsが最大 | 静的生成のエコシステムは成熟 |
| データの所在 | Supabase上。いつでもエクスポート可能 | 自分のDB | 何を繋ぐか次第 | ローカルファイルそのもの |
この組み合わせの本当の強み
- コストゼロ、運用ゼロ。 パッチを当てるサーバーも、バックアップスクリプトを書くDBも、証明書の更新もありません。個人サイト最大の隠れたコストは「落ちないか心配すること」ですが、この構成はそれすらゼロにします。
- 公開すれば即オンライン。 SSRはリクエストごとに問い合わせるので、保存した瞬間に読者へ届きます。「ローカルビルド → プッシュ → デプロイ待ち」という連鎖がここには存在しません。
- 管理画面込みで、DB機能も本物。 記事、コメント、カテゴリ登録簿、サイト設定がすべてPostgresに入り、RLSが「誰が何を読めるか」を明確に線引きします。SQLを直接書いて集計もできる——これはファイルベースの方式では得られないものです。
- 3言語を単一ソースで、SEOも標準装備。 RSS、sitemap、canonical、hreflangはすべてアーキテクチャ由来で、プラグインを寄せ集めたものではありません。
正直に伝えておくべき弱み
- AstroのエコシステムはNext.jsより小さい。 ニッチな問題に当たると、日本語で見つかる情報は限られています。英語のドキュメントを読むか、AIに直接聞くことのほうが多いでしょう。
- アクセス速度は「どの地域の読者に向けるか」で決まります。 CDNのエッジは拠点のある場所でしか速くならないので、Cloudflareが速いかどうかはCloudflareの問題ではなく、読者がどこにいるかの問題です。中国本土向けのサイトなら、そこでのCFのエッジは使えるもののVercel/Netlifyや地域CDNより体感で一段劣ります。このサイトは「自分の記録」が目的で、特定地域の速度は追っていません。読者の多い地域が決まっているなら、その地域の実測値を確認してから決めるのがおすすめです。
- 記事の公開はネット回線次第。 管理画面はブラウザからSupabaseへ直接接続するため、回線が不安定だとログインや保存でくるくる回ることがあります。良い回線に変えれば済む話で、読者側には一切影響しません。
- Supabaseの無料プロジェクトは7日間アクセスがないと一時停止します。 ダッシュボードで一度起こしてあげる必要があります。週次更新のブログなら問題ありませんが、「年次更新」の方はお忘れなく。
- デフォルトではページ単位のキャッシュなし。 公開ゼロ遅延の代償は、リクエストごとのリアルタイムレンダリングです(実際にはSSRは10msの上限を大きく下回ります)。必要なら
s-maxageを自分で足せばよく、個人サイトでは現状不要です。 - 長期的なリスク。 Cloudflare/Supabaseの無料枠の条件が将来変われば、移行や課金が必要になるかもしれません。無料枠頼みの構成に共通するリスクで、心の準備はしておくべきです。
ひとことでまとめると、「自分のもので、長く書き続けられて、手間のかからない」個人サイトが欲しいなら、この構成はとてもよく合います。対象地域でのアクセス速度が重視なら、あるいは豊富なプラグインを求めるなら、WordPressや静的サイトジェネレーターのほうが楽かもしれません。
私のサイト遍歴(なぜ3回目なのか)
個人サイトを持つのは今回が初めてではありません。
2018年、GitHubで初めてオープンソースの世界に触れ、あるブログプロジェクトのREADMEを頼りにローカルで一歩ずつ再現しました。動いた瞬間、新しい世界の扉が開いたのです。その後、Alibaba Cloudでドメインとサーバーを買い、無料のオブジェクトストレージとCDNをかき集めてデプロイしました。規制の都合で、工業情報化部のICP登録と公安の網絡安全登録を終えてから、ようやく正式に公開できました。コードの外側の暮らしについて書いていた数年間でしたが、コストの都合で5年後にサイトを閉じ、登録も解除しました。
2026年10月、AIによるプログラミングは、普通の人でも対話だけでシステムを組み立てられるほど成熟していました。もう一度作ろうと決めましたが、今回は発想を根本から変えています。従来のWordPressやHexoのやり方は自分の要望に合いませんでした(前者はサーバー運用が必要、後者は記事を出すたびにローカルビルドが必要)。そこで選んだのが「全マネージド・サーバーレス」の組み合わせです。コードはGitHub、ホスティングはCloudflare、データはSupabase、フロントは静的ページ+管理画面から直接公開。アクセス数が多くない個人サイトなら、これらの無料枠で十分すぎます。
こうして、いまご覧のサイトが生まれました。それでは本題に入ります。
ステップ1:準備
必要なアカウント
| サービス | 用途 | 料金 |
|---|---|---|
| GitHub | コードリポジトリ、デプロイのトリガー | 無料 |
| Cloudflare | Worker、DNS、HTTPSのホスティング | 無料 |
| Supabase | データベース+認証 | 無料 |
| ドメイン | ネット上の住所 | すでに持っていれば追加費用なし |
ローカル環境
- Node.js 22(リポジトリの
.nvmrcで固定。nvm useでOK) - git、npm
- GitHubとSupabaseにストレスなくアクセスできる回線(理由は後述します)
ステップ2:データベースを構築する(Supabase)
- Supabaseで登録し、New projectで新規プロジェクトを作成します。リージョンはどこでも構いません(個人サイトでは距離はほとんど影響しません)。データベースのパスワードは必ず控えておいてください。
- プロジェクトの準備ができたらSQL Editorを開き、リポジトリの
supabase/ディレクトリにある5つのSQLファイルを、以下の順番で1つずつ実行します(すべて再実行可能なので、「すでに存在します」というエラーは正常です)。1つも省いてはいけません。どれか欠けると、管理画面の対応機能がエラーになります。schema.sql:中核となる土台。記事、コメント、サイト設定、旧カテゴリリンクのマッピングといった主要テーブルを作り、RLS(行レベルセキュリティ)ポリシー、コメント関連のRPC関数、公開読み取り可能なimagesバケットを整えます。他のファイルはすべてこの上に積み重なるので、必ず最初に実行します。categories.sql:カテゴリ登録簿を作ります。カテゴリは登録簿が正とされ、追加/改名/統合/削除に対応。改名後は旧リンクが自動で301になり、404になりません。posts_author.sql:記事テーブルにauthor(著者)列を追加します。飛ばさないでください。飛ばすと管理画面からの公開がエラーになります。media_library.sql:ファイル管理に必要な記録テーブルを作ります。アップロードした画像や添付ファイルが「どの記事に属するか」を記録でき、記事削除時に正確に後片付けできるので、ストレージに参照されない孤立ファイルが溜まりません。posts_trash.sql:記事にソフト削除機能を追加します。記事の削除は「ゴミ箱へ移動」になり、5日間保持された後に自動で完全削除されます。その間は復元も手動の完全削除も可能です。
- Project Settings → APIを開き、後で使う2つの値をコピーします。
Project URL:https://xxxx.supabase.coの形式anon publicキー:eyJ...で始まる長い文字列
- 管理者アカウントを作成:Authentication → Users → Add userで、ご自身のメールアドレスとパスワードを入力し、自動確認にチェックを入れます。これが
/adminのログイン情報になります。当サイトの管理画面には独自の登録機構はなく、Supabaseのアカウント体系をそのまま使います。一人で使うサイトに、これ以上複雑な仕組みは要りません。
anonキーの安全性について。これはもともと公開される前提の設計で、フロントエンドと一緒に配布されます。本当の安全境界はRLSです。読者は「公開済みの記事」と「承認済みのコメント」しか読めず、書き込みもポリシー層で強制検証されます(ニックネーム/メール/本文の長さと形式)。機密性の高いもの(AIサービスのAPIキーなど)はCloudflare Secretに別途置き、ブラウザには渡しません。
ステップ3:ローカルでサイトを動かす
git clone https://github.com/<あなたのユーザー名>/<リポジトリ名>.git
cd <リポジトリ名>
nvm use # またはNode 22であることを確認
npm install
.env.exampleを.envにコピーし、ステップ2で取得した2つの値を入れます。
PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
PUBLIC_SUPABASE_ANON_KEY=eyJ...あなたのanonキー
# サイトの公開URL(sitemap / RSS / canonicalに影響)。ローカル開発では空でも可
PUBLIC_SITE_URL=
そのあと、
npm run dev # http://localhost:4321
.envがなくても動きます。その場合は内蔵の3言語サンプル記事にフォールバックするので、まず見た目を確認できます。「実際のデータベースに繋いだ状態」を試したいときに.envを埋めてください。
本番ビルドの成果物をローカルで確認するには、npm run build && npx wrangler pages dev distを使います(Workersのローカルエミュレーションで、本番のランタイムと一致します)。
ステップ4:GitHubにプッシュする
git add .
git commit -m "init"
git remote add origin https://github.com/<あなたのユーザー名>/<リポジトリ名>.git
git push -u origin main
以降の「コードを変更する」流れはすべて同じです。ローカルで編集 → コミット → プッシュ。このプッシュという行為そのものがデプロイのトリガーになります。
ステップ5:Cloudflare Workersへデプロイ(核心のステップ)
⚠️ このステップには大きな落とし穴が1つあります。プロジェクト作成時に選ぶのはWorkersで、Pagesではありません。当サイトはSSRアプリ(ビルドが生成するWorkerランタイムに依存)なので、必ずWorkers経由でデプロイします。間違えると後で
Workers-specific command in a Pages projectというエラーが出て、プロジェクトを削除して作り直すことになります。
- Cloudflare Dashboard → Workers & Pages → Create → Connect to GitでGitHubを認可し、リポジトリを選択、Production branchに
mainを指定します。 - ビルド設定(Framework presetでAstroを選べば大抵自動で埋まります):
- Build command:
npm run build - Build output directory:
dist - Deploy command:
npx wrangler deploy← リポジトリ直下のwrangler.tomlが制御します。変更しないでください
- Build command:
- Settings → Builds → Build environment variables(ビルド変数の欄です。Runtime変数の欄ではないので注意)に4つ追加します。
変数 値 説明 PUBLIC_SUPABASE_URLhttps://xxxx.supabase.co必須 PUBLIC_SUPABASE_ANON_KEYeyJ...必須 PUBLIC_SITE_URLまず https://<プロジェクト名>.workers.dev推奨。ドメイン紐付け後に変更 NODE_VERSION22.nvmrcとの二重チェックPUBLIC_*の3つはビルド時にインライン展開される変数です。値はnpm run buildの瞬間に成果物へ書き込まれるため、構築段階で変更した場合は再デプロイしないと反映されません。NODE_VERSIONは別物です。ビルド段階で使うNodeを決めるだけで成果物には入りませんが、変更時は同じく再デプロイが必要です。 - Save and Deployを押し、1〜3分待つと
https://<プロジェクト名>.workers.devが得られます。 - 以降は:コードを変えたときだけプッシュが必要です。記事の公開ではビルドは一切走りません。
wrangler.tomlはリポジトリに同梱済みです。核心はこの数行だけで、手動設定は不要です。
name = "mystyle"
main = "dist/_worker.js/index.js"
compatibility_date = "2025-07-18"
compatibility_flags = ["nodejs_compat"]
[assets]
directory = "./dist"
実際のファイルにはaccount_idと[vars]セクションもあります(後者は機密でない実行時設定を置きます)。account_idはご自身のCloudflareアカウントの値です。この記事のものをそのままコピーしないでください。リポジトリやアカウントを変えたときは忘れずに更新を。
nodejs_compatはSSRの生命線です。これが欠けると、Worker内のNode組み込み呼び出しはすべて即500になります。
ステップ6:独自ドメインを紐付ける
ドメインは昔Alibaba Cloudで取得したもの(.cn)です。接続手順は以下です。
- ドメインをCloudflareにホストする(2025年のコンソール刷新後の新しい導線):Dashboard左側のDomains → Onboard a domain → apexドメインを入力 → Continue。プランはFreeを選択。
- Cloudflareが2つのネームサーバー(
xxx.ns.cloudflare.com)を提示します。ドメイン登録事業者の管理画面で、DNSサーバー(NS)をこの2つに変更します。どの事業者にも該当の入口があり、多くの場合「ドメイン管理 → DNS変更/ネームサーバー」といったメニューにあります。 - CFのパネルでドメインの状態がActiveになるまで待ちます(NSの反映で通常数分〜数時間)。
- Workersプロジェクト → Settings → Triggers → Custom Domains → Addでドメインを入力。DNSがCF上にあればレコードは自動設定され、HTTPS証明書も自動発行。すべて無料です。
- 最後に忘れずに。ビルド環境変数の
PUBLIC_SITE_URLをhttps://あなたのドメインに変更し、Retry deploymentを実行します(Deploymentsページの最新デプロイ →···→ Retry)。これでsitemap、RSS、canonical内のドメインがすべて正しくなります。記事の内容には影響しません。
ステップ7:管理画面にログインして、最初の記事を公開する
ブラウザで<ドメイン>/adminを開き、ステップ2で作ったSupabaseアカウントでログインします。管理画面は4つのモジュールに分かれています。
- 記事管理:タイトル、言語(中/英/日)、カテゴリ、本文(Markdownのリアルタイムプレビュー)を入力し、「公開」を押します。公開すれば即反映——フロントを更新すれば、記事がすでに待っています。エディタの「詳細設定」では公開日時も変更できます(フロントの表示、並び順、RSSに反映)。混乱しやすい点を2つ先に説明します。
- 言語ボタンを押すと「バージョン切替」になります。 言語フラグを変えるだけでなく、その言語で公開済みのタイトル、抜粋、本文、slugをそのまま読み込みます。切り替えたら別言語の内容が出てくるのは正常な動作です。
- 記事の削除はゴミ箱行きです。 すぐには消えず、5日間保持された後に自動削除されます。その間は復元も手動の完全削除も可能です。
- ファイル管理:画像や添付ファイルをアップロードし、各ファイルがどの記事に属するかを自動記録します。記事削除時には連動して後片付けするので、ストレージに参照されない孤立ファイルは残りません。
- コメント管理:承認、返信(管理者の返信には専用の印が付き、ネストして表示されます)、削除。
- その他の機能:トップページのカウントダウン、抜粋生成の設定、カテゴリ管理(改名で自動301)、サイトのアバター画像アップロード(過去のアバターへの切替・見返しを含む)。
コメント設計について一言。コメントテーブルはメールアドレスを収集しますが公開はしません。ブログ主の管理画面からのみ見えます。匿名のコメント投稿者には削除トークンが発行され、自分が書いたコメントを管理できます。全員に登録してもらう必要はありません。
ステップ8:3言語とSEO
3言語対応はこのアーキテクチャ標準の機能で、追加サービスは不要です。言語ごとに独立したRSS、サイト全体のsitemapがあり、canonicalとhreflangも設定済みです。各言語版は独立したレコードなので、管理画面で言語を切り替えながらそれぞれ執筆・公開します。ある言語版が未公開の場合、フロントではお知らせバナーを表示し、既存のバージョンはそのまま残します。空白ページにはなりません。
つまずきの記録(この記事でいちばん価値のある部分)
以下はすべて私が実際に踏んだもので、「現象 → 原因 → 解決」の形で記録します。
1. プロジェクト作成時にPagesを選んでしまい、デプロイが即エラー
現象:Workers-specific command in a Pages project。
原因:当サイトはSSR(成果物にWorkerランタイムを含む)で、デプロイコマンドはnpx wrangler deploy。これはWorkersのデプロイ方式であり、Pagesプロジェクトのビルドモデルとは互換性がありません。
解決:作成時にWorkers → Connect to Gitを選ぶ。Pagesは選ばない。
2. サイト全体が500、ログにNode組み込みモジュールの文字
原因:nodejs_compat互換フラグが欠けており、SSRが使うNodeの機能がWorkerランタイムで利用できませんでした。
解決:wrangler.tomlにcompatibility_flags = ["nodejs_compat"]を含める(リポジトリには設定済み)。
3. sitemap / RSSのドメインが間違っている
現象:成果物にyour-domain.comというプレースホルダが出る、あるいは本番のcanonicalがexample.comを指す。
原因:PUBLIC_SITE_URLが未設定。さらに隠れた層として、Astroは.envをastro.config.mjsの読み込み時点のprocess.envへ注入しません。値を取得できるのはimport.meta.envだけなので、設定ファイル内でprocess.env.PUBLIC_SITE_URLを直接読むと静かに空文字が返り、既定のサイトアドレスにフォールバックします。
解決:astro.config.mjsでViteのloadEnvを使い、ファイルの環境変数を明示的に読むようにし、CIのprocess.envによるビルド変数にも対応。両方の経路を塞ぎました。変更後はRetry deploymentを。
4. 管理画面からの公開でエラー(author列がない)
原因:記事テーブルには初期状態でauthor列がなく、公開処理は書き込み時にそれを必須としていました。
解決:supabase/posts_author.sqlを実行して列を追加。ステップ2で「5つのSQLファイルを1つも省くな」と強調したのはこのためです。どれか1つ欠けると、管理画面の対応機能がエラーになります。
5. 管理画面で変えた環境変数が「謎の巻き戻り」
現象:Cloudflareのダッシュボードで設定変数の値を変えたのに、しばらくすると古い値に戻っている。
原因:デプロイコマンドがnpx wrangler deployで、wrangler.tomlの[vars]がデプロイのたびに同期されます。そのためダッシュボードで変えた通常の変数が、リポジトリ側の値で上書きされることがあります(Secretは影響を受けないので、「公開変数は戻ったのに鍵は変わっていない」という妙な組み合わせになりがちです)。
解決:リポジトリのwrangler.tomlとダッシュボードの変数を一致させます。最終的にtomlをダッシュボードと同じ値にして両者を揃え、どちらからも黙って上書きされないようにしました。設定変更後はRetry deploymentを忘れずに。
6. 地域によっては、管理画面が回線に左右される
原因:管理画面はブラウザからSupabase(*.supabase.co)へ直接接続しますが、このエンドポイントへの直結は多くのネットワークで不安定です(私が実感しているのは中国本土ですが、そこだけの話ではありません)。一方、読者側はCloudflareのエッジ経由なのでまったく影響を受けません。
解決:記事を書くときは快適な回線で管理画面にログインすればよく、読者側は何も気づきません。これはアーキテクチャの帰結でもあります。読み取り経路と書き込み経路が分離されており、読者側はSupabaseの到達性に依存しません。
7. Workersの無料枠は気にしなくていいのか? 実測ベースの目安:ビルドは1回1〜3分、月3,000分は個人サイトなら使い切れません。ランタイムは1日10万リクエスト、1回のCPU時間10msで、SSRレンダリングは通常その10msを大きく下回ります。唯一気をつけるのは「コードを変えたときだけビルドが走る」こと——記事の公開ではビルド分数を消費しません。
費用の内訳
| 項目 | 費用 |
|---|---|
| ドメイン | 唯一の過去支出(昔に取得、年単位で更新) |
| Cloudflare Workers / DNS / HTTPS | 0円 |
| Supabase(DB+認証+画像ホスティング) | 0円(無料枠) |
| GitHub | 0円(公開リポジトリ) |
| 月額合計 | 0円 |
日々のメンテナンス
- 記事の投稿・修正:
/adminで操作。即反映、デプロイ不要。 - コード/スタイル/機能の変更:ローカルで編集 → プッシュ → CFが自動ビルド(1〜3分)→ 完了。
- 設定変更(環境変数、ビルドパラメータ):Cloudflareのダッシュボードで変更 → Retry deployment。
- データのバックアップ:Supabaseからいつでもエクスポート可能。記事自体がMarkdownのテキストなので、DBさえあれば何も失いません。
- 証明書と更新:Cloudflareが自動。何もしなくて大丈夫です。
おわりに
2018年の、手作業で運用する必要があったAlibaba Cloudのサーバーから、2026年のこの「プッシュすればデプロイ、公開すればオンライン」という全マネージドの組み合わせへ。変わったのは技術スタックだけではなく、個人サイトを持つためのハードルの高さそのものです。「運用担当者であること」から、「GitHubのアカウントがあればいい」まで下がりました。そして今回の再構築は、最後までAIの力を借りて進めました。私はSupabaseの専門家ではありませんが、何を訊くべきかは分かっていました。
この記事を読んで同じようにサイトを作った方がいれば、コメント欄に報告しに来てください。
いつものように、一貫して。
コメント0
まだコメントはありません。最初の一本を。