ゼロから公開まで:個人サイトの完全デプロイ記録

個人サイトの完全なデプロイ記録です。Astro + Supabase + Cloudflare Workers というフルマネージドな構成で、ゼロコストで中英日三言語対応のブログを立ち上げました。記事は二部構成——前半はゼロから同じサイトを作れるデプロイ手順、後半は実際に踏んだ九つの落とし穴を、原因と解決策つきでまとめています。核心は「ゼロ運用・公開すれば即反映・月額 0 円」。

ゼロから公開まで:個人サイトの完全デプロイ記録

はじめに

この記事は、このサイトの「告白書」です——一台のローカル PC 上のコードが、いまあなたがご覧になっているこのページになるまでの全記録です。内容は二部構成。前半はゼロから始める完全なデプロイ手順(そのまま進めれば、まったく同じサイトが出来上がります)。後半は実際に踏んだ落とし穴の記録(どれも私が実際に踏んだもので、原因と解決策も書き記しています)。

誰に向けた記事か——ゼロコストで個人ブログを立ち上げたい人。あるいは未来の自分自身へ。いつかまた一から作り直したくなったとき、何もないところから手探りしなくて済むように。

完成形の概要:どんなサイトなのか

まずは完成後の姿を見て、全体像をつかんでください。

項目 現状
フレームワーク Astro 4(SSR サーバーサイドレンダリングモード)
データベース Supabase(Postgres。記事/コメント/カテゴリ/サイト設定はすべてここ)
ホスティング Cloudflare Workers(無料枠:10 万リクエスト/日、帯域無制限、ビルド 3000 分/月)
ドメイン 自前取得のドメイン。DNS は Cloudflare で管理、HTTPS は自動発行
言語 中国語 / English / 日本語 の三言語を同一ソースから
執筆バックエンド 自前の /admin 管理画面。ブラウザで直接書いて直接公開
ファイル管理 画像と添付ファイルを一元管理。記事を削除すると関連ファイルも自動で整理され、孤立ファイルはワンクリックで一掃
要約 本文から自動生成(ローカルルールによるフォールバック、または AI 生成)。一覧カード・検索・RSS・シェアカードで使用
翻訳 管理画面からワンクリックで英/日版を生成(智譜 GLM の無料枠)。長文は構造を認識した分割と並行翻訳に対応
月額コスト 0 円(ドメインのみ過去の支出。それ以外はすべて無料枠)

その仕組みを一言でいえば——読者がアクセスするのは常に Cloudflare で、Cloudflare はリクエストのたびに Supabase へリアルタイムで問い合わせて記事を返します。だから管理画面で「公開」を押したその瞬間に記事が公開されます。ビルドを待つ必要も、再デプロイも不要——これがこの設計で私が最も気に入っている点です。

読者のブラウザ ──► Cloudflare Worker(SSR レンダリング + 静的アセット)
                     │ 毎リクエストでリアルタイム照会
                     ▼
              Supabase(記事 / コメント / カテゴリ / 設定)
                     ▲
管理画面 /admin ──────┘  (ブラウザが Supabase に直結、公開は即反映)

方式の比較:なぜこの構成なのか、そしてその弱点

技術選定は長所だけで判断できません。主要な選択肢を並べて正直に比較してこそ、この構成が自分に合うかどうかを判断できます。

主要な選択肢との横並び比較

観点 本構成(Astro + Supabase + CF Workers) WordPress Next.js + Vercel Hexo / Hugo(完全静的)
サーバー運用 不要、フルマネージド 必要(またはマネージド契約) 不要 不要(ただし公開時にローカルビルド)
公開フロー 管理画面から直接公開、即時反映 管理画面から公開 同じ ローカルで執筆 → ビルド → push
オンライン執筆 ✅ 管理画面内蔵 ✅ 別途 CMS が必要 ❌
データベース Supabase(Postgres、完全な SQL) MySQL まちまち(外部 CMS 接続が多い) なし(ファイルがコンテンツ)
無料枠 CF 10 万リクエスト/日 + Supabase 無料枠 なし(ホスティング費が必要) あり(商用利用に制限) 静的ホスティングはほぼ無制限
中国からのアクセス CF エッジはやや遅いが利用可 ホスト次第 同様に不安定 中国国内 CDN が使えれば最速
エコシステム/情報量 Astro は小さめ、中国語情報が少ない 最大。プラグインもテーマも豊富 Next.js が最大 静的生成のエコシステムが成熟
データの所有 Supabase 上にあり、いつでもエクスポート可 自分の DB 接続先次第 ローカルファイルそのもの

この構成の本当の強み

  • ゼロコストかつゼロ運用:サーバーのパッチ当てなし、DB バックアップスクリプトなし、証明書は自動更新。個人サイト最大の隠れコストは実は「壊れないかという不安」で、この構成はそれすらゼロにします。
  • 公開すれば即反映:SSR は毎リクエストで照会するので、公開を押した瞬間に読者に見えます。完全静的構成の「ローカルビルド → push → デプロイ待ち」という流れはここには存在しません。
  • 管理画面内蔵+データベース機能が完全:記事・コメント・カテゴリ登録表・サイト設定がすべて Postgres 上にあり、RLS が「誰が何を読めるか」を明確に管理。SQL を直接書いて集計もできます——ファイル型の構成にはできない芸当です。
  • 三言語を同一ソースで、SEO も内蔵:RSS、sitemap、canonical、hreflang はすべてアーキテクチャ標準装備で、プラグインの寄せ集めではありません。

はっきり書いておくべき弱点

  • Astro のエコシステムは Next.js より小さい:マイナーな問題に当たると、検索で見つかる中国語の解決策は限られ、多くの場合英語のドキュメントを読むか AI に直接聞くことになります。
  • 中国からの Cloudflare エッジは「使えるがやや遅い」:これは CF の中国本土ネットワークの客観的な現状で、読者の体感は Vercel/Netlify や中国国内 CDN より一段劣ります。このサイトは「自分自身の記録」と位置づけているので国内最速は狙っていませんが、中国国内の読者向けコンテンツサイトを作るなら、ここは真剣に天秤にかけるべきです。
  • 管理画面からの公開はネットワーク次第:管理画面はブラウザから Supabase に直結するため、中国国内の不安定な回線ではログインや保存がぐるぐる回ることがあります。快適な回線に切り替えればよく、読者側には影響しません。
  • Supabase 無料プロジェクトは 7 日間の未使用で一時停止:管理画面に入って一度起こす必要があります。週次更新のブログなら影響なしですが、「年次更新」の人は覚えておきましょう。
  • 既定ではページレベルのキャッシュなし:公開ゼロ遅延の代償は、毎リクエストがリアルタイムレンダリングになることです(幸い SSR は通常 10ms の上限をはるかに下回ります)。本当にキャッシュが必要になったら s-maxage を自分で足せばよく、個人サイトでは今のところ不要です。
  • 長期的なリスク:無料枠のポリシー(Cloudflare / Supabase / 智譜)が将来変われば、移行や有料化が必要になるかもしれません——これは「無料枠で生きる」あらゆる構成に共通するリスクで、心づもりが必要です。

一言でまとめると——「自分のもので、長く書き続けられ、手間のかからない」個人サイトが欲しいなら、この構成はよく合います。中国国内のアクセス速度や豊富なプラグインを求めるなら、WordPress や静的構成のほうが楽かもしれません。

私のサイト構築小史(なぜ三度目なのか)

個人サイトを持つのは、これが初めてではありません。

2018 年、私は GitHub で初めてオープンソースの世界に触れました。あるブログプロジェクトの README を追いながらローカルで一歩ずつ再現し、無事に動いた瞬間、新しい世界の扉が開きました。その後、阿里雲(アリババクラウド)で自分のドメインとサーバーを購入し、無料のクラウドストレージと CDN を活用してサイトをデプロイ。規制上の要請から、工業和信息化部(工信部)の ICP 届出と公安のネットワーク安全届出を完了して、ようやく正式に公開できました。あの数年はコードの外の暮らしを綴っていましたが、コストを理由に五年後にサイトを閉鎖し、届出も取り消しました。

2026 年 10 月、AI によるプログラミングは、普通の人が対話だけでシステムを構築できるほど成熟していました。私はもう一度作ることにしました——ただし今回は発想を根本から変えて。従来の WordPress や Hexo のやり方は私のニーズに合いませんでした(片方はサーバー運用が必須、もう片方は投稿のたびにローカルビルドが必要)。最終的に選んだのは「フルマネージド・サーバーレス」の組み合わせです——コードは GitHub、ホスティングは Cloudflare、データは Supabase。フロントは静的ページ、バックエンドの管理画面から直接投稿。アクセス数の多くない個人サイトなら、これらのサービスの無料枠で十分すぎるほどです。

こうして、いまご覧いただいているこのサイトが生まれました。それでは本題に入ります。

ステップ 1:準備

アカウント一覧

サービス 用途 料金
GitHub コードリポジトリ、デプロイのトリガー 無料
Cloudflare Worker ホスティング、DNS、HTTPS 無料
Supabase データベース + ログイン認証 無料
智譜オープンプラットフォーム 翻訳 API(任意) GLM-4.7-Flash はモデル単位で無料
ドメイン一つ 住所 すでに取得済みなら追加費用なし

ローカル環境

  • Node.js 22(リポジトリの .nvmrc で固定されているので nvm use でOK)
  • git、npm
  • GitHub と Supabase に快適にアクセスできるネットワーク環境(理由は後述)

ステップ 2:データベースを構築する(Supabase)

  1. Supabase に登録し、New project でプロジェクトを新規作成。リージョンは任意で構いません(個人サイトでは距離はさほど影響しません)。データベースのパスワードは必ず控えておいてください。
  2. プロジェクトの準備ができたら SQL Editor に入り、リポジトリの supabase/ ディレクトリにある五つのファイルを順番に一つずつ実行します(いずれも再実行可能で、「already exists」というエラーが出ても正常です):
    • schema.sql:核心。posts(記事)、comments(コメント)、site_settings(サイト設定)、category_aliases(カテゴリ旧リンクの 301 マッピング)の四つのテーブルを作成し、RLS 行レベルセキュリティポリシー、コメント関連の三つの RPC 関数、そして公開読み取り可能な images 画像バケットを設定します。
    • categories.sql:カテゴリ登録表を作成。カテゴリは登録表を正式な情報源とし、追加・リネーム・統合・削除に対応。リネーム後は旧リンクが自動で 301 され、404 になりません。
    • posts_author.sql:記事テーブルに author(著者)列を追加します。この手順は飛ばさないでください。飛ばすと管理画面からの公開時にエラーになります。
    • posts_trash.sql:記事のゴミ箱(ソフトデリート)。記事の削除は「ゴミ箱へ移動」に変わり、5 日間保持された後に完全削除されます。フロント側の RLS フィルタと定期クリーンアップ関数も含みます。飛ばさないでください。飛ばすと削除機能が使えません。
    • media_library.sql:ファイル管理(メディアライブラリ)。各ファイルと記事の紐付けを記録する media テーブルを作成し、attachments 添付バケット(pdf/word/ppt/excel などに対応)、さらに「孤立ファイル」検出ビューを追加します。飛ばさないでください。飛ばすと管理画面の「ファイル管理」モジュールが使えません。

      三つだった SQL が五つになったのは、この記事の初公開後に追加された機能のためです——いま手順どおりに進めれば、完全版が手に入ります。

  3. Project Settings → API Keys に進み、後で使う二つをコピーします(Supabase の新しいコンソールではこの項目はここにあります):
    • Project URL:https://xxxx.supabase.co の形式
    • anon public key:eyJ... で始まる長い文字列
  4. 管理者アカウントを作成:Authentication → Users → Add user で、メールアドレスとパスワードを入力し、自動確認にチェックを入れます。これが管理画面 /admin のログイン情報です——当サイトの管理画面には独自の登録システムはなく、Supabase のアカウント体系をそのまま使います。一人のサイトにこれ以上複雑なものは不要です。

anon key の安全性について:これはもともと公開される前提のもので、フロントエンドに配信されます。本当のセキュリティ境界は RLS です——読者が読めるのは「公開済みの記事」と「承認済みのコメント」だけで、書き込み操作もポリシー層で厳格に検証されます(ニックネーム/メール/本文の長さと形式)。機密を要する操作(翻訳 API key など)は Cloudflare の Secret に別途保管し、ブラウザには配信しません。

ステップ 3:サイトをローカルで動かす

git clone https://github.com/<あなたのユーザー名>/<リポジトリ名>.git
cd <リポジトリ名>
nvm use          # または Node 22 であることを確認
npm install

.env.example を .env にコピーし、ステップ 2 で取得した二つの値を入力します:

PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
PUBLIC_SUPABASE_ANON_KEY=eyJ...あなたの anon key
# サイトの公開URL(sitemap / RSS / canonical に影響);ローカル開発では空のままで可
PUBLIC_SITE_URL=

そして:

npm run dev      # http://localhost:4321

.env を設定しなくても動きます——サイトは内蔵の三言語サンプル記事にフォールバックし、まず効果を確認できます。「実際のデータベースに接続した」完全な状態を検証したいときに .env を埋めてください。

本番ビルドの成果物をローカルでプレビューするには npm run build && npx wrangler dev を使います(Workers ランタイムをローカルで再現し、本番と同一。wrangler はリポジトリの wrangler.toml を読み込みます)。wrangler pages dev は使わないでください——本プロジェクトは Pages ではなく Workers で動いており、コマンドと成果物が噛み合いません(ステップ 5 の落とし穴を参照)。

ステップ 4:GitHub にプッシュする

git add .
git commit -m "init"
git remote add origin https://github.com/<あなたのユーザー名>/<リポジトリ名>.git
git push -u origin main

以降の「コードを変更する」流れはすべて同じです:ローカルで変更 → commit → push。push という行為そのものがデプロイのトリガーになります。

ステップ 5:Cloudflare Workers へデプロイする(核心ステップ)

⚠️ この段階には大きな落とし穴が待っています。プロジェクト作成時に選ぶのは Workers であり、Pages ではありません。当サイトは SSR アプリ(ビルドで生成される Worker ランタイムに依存)なので、必ず Workers 経由です。間違えると後で Workers-specific command in a Pages project というエラーが出て、削除して作り直す羽目になります。

  1. Cloudflare Dashboard → Workers & Pages → Create → Connect to Git で GitHub を認可し、リポジトリを選択、Production branch を main に設定します。
  2. ビルド設定(Framework preset で Astro を選べば通常は自動入力されます):
    • Build command:npm run build
    • Build output directory:dist
    • Deploy command:npx wrangler deploy ← リポジトリ直下の wrangler.toml が制御します。変更しないでください
  3. Settings → Builds → Build environment variables(ビルド変数の欄であり、Runtime 変数の欄ではない点に注意)に四つ追加します:
    変数 値 説明
    PUBLIC_SUPABASE_URL https://xxxx.supabase.co 必須
    PUBLIC_SUPABASE_ANON_KEY eyJ... 必須
    PUBLIC_SITE_URL まず https://<プロジェクト名>.workers.dev を入力 推奨。ドメイン紐付け後に変更
    NODE_VERSION 22 .nvmrc との二重チェック

    この三つの PUBLIC_* はビルド時にインライン展開される変数です——値は npm run build の瞬間に成果物へ書き込まれます。ですから構築段階でこれらを変更した場合、再デプロイしないと反映されません。

  4. Save and Deploy を押し、1〜3 分待つと https://<プロジェクト名>.workers.dev が得られます。
  5. 以降:コードを変更したときだけ push が必要です。記事の投稿はビルドを一切トリガーしません。

wrangler.toml はリポジトリに同梱済みで、中身はこれだけです。手動設定は不要です:

name = "mystyle"
main = "dist/_worker.js/index.js"
compatibility_date = "2025-07-18"
compatibility_flags = ["nodejs_compat"]

[assets]
directory = "./dist"

このうち nodejs_compat は SSR の生命線です——これが欠けると、Worker 内のあらゆる Node 組み込み呼び出しが即座に 500 になります。

ステップ 6:独自ドメインを紐付ける

ドメインは私が昔に阿里雲(アリババクラウド)で購入したもの(.cn)です。接続の流れは次のとおりです:

  1. ドメインを Cloudflare でホストする(2025 年のコンソール改版後の新しい手順):Dashboard 左側の Domains → Onboard a domain → apex ドメインを入力 → Continue。プランは Free を選択。
  2. Cloudflare から二つの NS サーバー(xxx.ns.cloudflare.com)が提示されます。ドメイン登録業者の管理画面で、ドメインの DNS サーバー(NS)をこの二つのアドレスに変更してください——どこの登録業者にもこの入口があり、たいてい「ドメイン管理 → DNS 設定 / ネームサーバー」といったメニューにあります。
  3. CF パネルでドメインのステータスが Active になるまで待ちます(通常は数分から数時間、NS の反映時間によります)。
  4. Workers プロジェクト → Settings → Triggers → Custom Domains → Add でドメインを入力します。DNS が CF 上にあるのでレコードは自動設定され、HTTPS 証明書も自動発行——すべて無料です。
  5. 最後の一手を忘れずに:ビルド環境変数の PUBLIC_SITE_URL を https://あなたのドメイン に変更し、Retry deployment を実行します(Deployments ページの最新デプロイ → ··· → Retry)。これで sitemap、RSS、canonical のドメインがすべて正しくなります。記事の内容には影響しません。

ステップ 7:管理画面にログインして最初の記事を投稿する

ブラウザで <ドメイン>/admin を開き、ステップ 2 で作成した Supabase アカウントでログインします。管理画面には四つのモジュールがあります:

  • 記事管理:タイトル、言語(中国語/英語/日本語)、カテゴリ、本文(Markdown のリアルタイムプレビュー付き)を入力し、「公開」を押します。公開は即反映——フロントを更新すれば、記事がすでに待っています。エディタの「詳細設定」では、公開日時(フロントの表示・並び順・RSS に同期)、Slug、カバー画像、要約(空欄なら公開時に本文から自動生成)も変更できます。記事の削除は「ゴミ箱へ移動」のソフトデリートで、5 日間保持された後に自動で完全削除され、その間はいつでも復元できます。
  • ファイル管理:本文中の画像や添付ファイル(pdf/word/ppt/excel など)を一元管理します。記事を削除すると、それが使っていたファイルも連動して整理されます。どの記事からも参照されていない「孤立ファイル」はワンクリックで一掃でき、ストレージの肥大化を心配する必要はありません。
  • コメント管理:承認、返信(管理者の返信には専用のマークが付き、ネスト表示されます)、削除。
  • その他の機能:トップページのカウントダウン、要約の自動生成設定(文字数上限と生成スタイル)、カテゴリ管理(リネームで自動 301)。

ついでにコメントの設計について一言:コメントテーブルはメールアドレスを収集しますが公開はせず、ブログ主の管理画面からのみ閲覧できます。匿名のコメント投稿者には削除トークンが発行され、自分が投稿したコメントを管理できます——誰も登録する必要はありません。

ステップ 8:任意の拡張機能

ワンクリック翻訳:管理画面のエディタで中国語を書き、「翻訳」を押すと英/日版が直接生成されます。OpenAI 互換プロトコルを使用し、現在は智譜 GLM-4.7-Flash(モデル単位で永久無料、個人利用なら上限を心配する必要はありません)に接続しています。互換サービス(DeepSeek、Moonshot、Gemini)に切り替える場合も、二つの変数を変えるだけでコード変更はゼロです:

変数 場所
TRANSLATE_BASE_URL / TRANSLATE_MODEL wrangler.toml の [vars]
TRANSLATE_API_KEY Cloudflare 管理画面 → Settings → Variables and Secrets → Add(Type: Secret)

長文の翻訳には専用のコツがあります——分割は構造を認識していなければならないということです。初期のバージョンは行数で機械的に切っていたため、12 列の表が真ん中で断ち切られ、繋ぎ直すと一つの表が四つに砕けて、とても読める訳文ではありませんでした。現在はまず構造を識別してから詰め込みます——コードブロックは丸ごと出入りし、表も丸ごと出入りし(巨大な表は行単位で分割し、各断片にヘッダーを繰り返すことで、どの断片もそれ自体が正しい表になります)、長すぎる段落は文の切れ目でのみ分割します。チャンク間は 2 並列で翻訳し(レート制限対策としてチャンク間に 1.5 秒の間隔)、前のチャンクの原文の末尾を「上文参照」として一緒に送り込むことで、チャンクをまたぐ指示語の断裂と用語のぶれを抑えます。失敗時は従来どおり指数バックオフで再試行します。

この変更は実際の失敗を解決しました:約 12,000 字の長文を逐次翻訳すると約 233 秒かかり、フロントの待機上限は 200 秒——必ずタイムアウトで失敗していました。並列化により約 140 秒に短縮され、窓に収まりました。

AI 要約:記事の要約は、一覧カード、記事冒頭のリード文、検索結果、RSS、ソーシャルシェアカードの五か所で使われます。執筆時に要約欄を空にしておくと、公開の瞬間に本文から自動抽出されます(見出し・コードブロック・表を飛ばし、叙述部分だけを取り出し、文の切れ目で止めて省略記号を付けます)。エディタで「✨ AI 生成」を押してモデルに書かせることもできます。文字数上限とスタイル(陳述式/箇条書き/フック)は管理画面の「その他の機能」で設定でき、既定は 200 字です。AI が利用できないときは自動でローカル抽出にフォールバックし、それが縮退した結果であることを画面上で明確に伝えます——AI の出力に見えるものを黙って差し込むことはありません。

三言語と SEO:言語ごとの独立した RSS、サイト全体の sitemap、canonical、hreflang はすべて標準装備です。記事に訳文がない場合、フロントは「訳文なし」のバナーを表示して原文を保持します。空白のページにはなりません。

ソーシャルシェアカード:WeChat/Weibo/Facebook へシェアする際のサムネイルと説明文は Open Graph プロトコルに従います。og:image は記事のカバー画像を優先し、未設定ならサイト既定の画像でフォールバックします。説明文は記事の要約を使います。つまり要約を空にしておくと、サイト内だけでなくシェアカードの説明文も空になるということです——これも上記の自動生成が必要な理由の一つです。

落とし穴の記録(この記事で最も価値のある部分)

以下はどれも私が実際に踏んだもので、「現象 → 原因 → 解決策」の形で記録しています。

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 を実行して列を追加。これが SQL ファイルを一つも飛ばせない理由です。

5. 翻訳サービスの切り替え後、「謎のロールバック」
現象:Cloudflare の管理画面で翻訳サービスを Gemini から智譜 GLM に変えたのに、しばらくすると Gemini に戻っている。
原因:デプロイコマンドが npx wrangler deploy で、wrangler.toml の [vars] はデプロイのたびに本番へ同期されます——管理画面で変更した通常の変数は、リポジトリの値に上書きされる可能性がある(Secret 型の API key は影響を受けないため、「モデルは変わったのに key は変わらない」という妙な組み合わせになる)。
解決策:リポジトリの wrangler.toml と管理画面の変数を一致させる。最終的に toml も智譜の値に変更し、両者を揃えて、どちらかが黙って他方を上書きすることがなくなりました。

6. 中国国内から管理画面へアクセスするのは「回線の機嫌次第」
原因:管理画面はブラウザから Supabase(*.supabase.co)に直結し、中国国内からの直結は不安定です。ただし読者側は Cloudflare のエッジを通るため、まったく影響を受けません。
解決策:投稿時に快適な回線で管理画面にログインすればよく、読者側は気づきません。これもアーキテクチャ設計の結果です——読み書きの経路を分離し、読者側は Supabase の到達性に依存しません。

7. Workers の無料枠は気にする必要があるのか?
実測ベース:ビルドは 1〜3 分/回、3000 分/月は個人サイトなら使い切れません。ランタイムは 10 万リクエスト/日、1 回あたりの CPU 10ms で、SSR レンダリングは通常 10ms をはるかに下回ります。唯一注意すべきは「コードを変更したときだけビルドが走る」——記事の投稿はビルド分を消費しません。

8. AI 要約が時々「サービスが空を返しました」と報告するのに、エラーは出ていない
現象:「✨ AI 生成」を押し、数秒後に要約生成の失敗が表示される。エラーは「翻訳サービスが空を返しました」なのに、翻訳機能はすべて正常。
原因:これが最も直感に反する落とし穴です——API は HTTP 200 を返し、エラーは一切ないのに、中身が空。当時接続していた智譜 GLM-4.7-Flash は推論型モデルで、まず思考プロセスを reasoning_content フィールドに書き出します。そして max_tokens は「思考 + 回答」の共有予算であり、回答だけに与えられるものではありません。要約リクエストには 1024 しか与えておらず、このモデルの思考は実測で 1300〜1600 文字に達するため、思考段階で予算を使い切り、回答を書く時点で残高がゼロになっていました。判別材料は、レスポンスの finish_reason が正常な stop ではなく length(長さによる打ち切り)であることです。
解決策:推論モデルには十分な予算を確保する。第一に、要約リクエストの max_tokens を 1024 から 4096 に引き上げる。第二に、呼び出し層に自己修復を追加——「中身が空 + finish_reason が length」を検知したら自動で予算を倍にして再試行する(実測では最初の倍化で回答が得られます)。あわせてエラーメッセージを問題を指し示す表現に変更し(「予算切れ」「思考だけ返して回答なし」「本当に空の中身」の三つを区別)、「空を返しました」と大雑把に言うのをやめました。最後にもう一段のフォールバックとして、AI が使えないときは自動でローカル抽出に回り、執筆が止まらないようにしました。

9. 長文の翻訳は必ず失敗するのに、短い記事は問題ない
現象:千字以内の記事は正常に翻訳できるが、一万字を超える長文は必ず失敗し、しかも長時間待った末にエラーになる。
原因:初期は各チャンクを逐次翻訳しており、チャンクごとのタイムアウト上限が 45 秒、チャンク間にも 2 秒の待機がありました。約 12,000 字の記事を 5 チャンクに分割すると、最悪の場合 5×45 + 4×2 ≈ 233 秒となり、フロントの待機上限は 200 秒——設計上、失敗は避けられません。回線の良し悪しとは無関係です。さらに分割器が表を真ん中で断ち切るため、繋ぎ直した訳文は壊れていました。
解決策:分割を構造認識に変更(コードブロックと表は丸ごと出入りし、巨大な表は分割後に各断片へヘッダーを繰り返す)。翻訳は 2 並列にして所要時間を約 140 秒に短縮。さらに各チャンクに「前のチャンクの原文の末尾」をコンテキストとして付けて、チャンクをまたぐ指示語の乱れを減らしました。

費用一覧

項目 費用
ドメイン 唯一の過去支出(昔に購入、年ごとに更新)
Cloudflare Workers / DNS / HTTPS 0
Supabase(データベース+認証+画像ホスティング) 0(無料枠)
GitHub 0(公開リポジトリ)
智譜 GLM(翻訳 + AI 要約) 0(GLM-4.7-Flash はモデル単位で無料)
月額合計 0 円

日々のメンテナンス

  • 記事の投稿/編集:/admin 管理画面で操作、即時反映、デプロイ不要。
  • コード/スタイル/機能の変更:ローカルで変更 → push → CF が自動ビルド(1〜3 分)→ 完了。
  • 設定の変更(環境変数、翻訳プロバイダ):Cloudflare 管理画面で変更 → Retry deployment。
  • データのバックアップ:Supabase からいつでもエクスポート可能。しかも記事そのものが Markdown テキストなので、データベースさえ手元にあれば何とかなります。
  • 証明書と更新:Cloudflare が自動で処理。何もする必要はありません。

おわりに

2018 年の手動運用が必要だった阿里雲のサーバーから、2026 年のこの「push すればデプロイ、投稿すれば公開」というフルマネージドの組み合わせへ——変わったのは技術スタックだけではなく、個人サイトを持つためのハードルそのものです。それは「運用担当者でなければならない」から「GitHub アカウントがあればいい」まで下がりました。そして今回の再構築は、すべて AI の力を借りて成し遂げました。私は Supabase に精通しているわけではありませんが、何を問うべきかは分かっていました。

この記事をなぞってあなたも一つ立ち上げたなら、コメント欄に報告しに来てください。

いつもどおり、一貫して。

コメント0

    ← ホームへ