Cloudflare Pages で個人サイトを立てて詰まった6箇所
目次
ドキュメントに書いていない方で詰まる
Astro の静的サイトを Cloudflare Pages に載せ、独自ドメインの割り当て、問い合わせ用メール、アクセス解析まで通した。手順自体は公式ドキュメントどおりで問題ない。詰まったのはそこに書かれていない挙動の方だった。
6箇所ある。UI の場所が想像と違うもの、選択肢が提示されず強制されるもの、そして自分の検証方法が間違っていたもの。
1. Framework preset が見つからない
Pages のプロジェクト作成は3ステップに分かれている。「Framework preset が無い」と感じるのは、Step 1(リポジトリ選択)を見ているからだ。Framework preset・Build command・Build output directory は Step 2(ビルド設定)にある。
もう一つ落とし穴がある。Cloudflare は Pages を Workers に統合しつつあり、Workers & Pages → Create の既定タブは Workers 側になっている。そちらの「Import a repository」に進むと、そもそも項目構成が違う。見分け方は単純で、
- Build output directory がある → Pages
- Deploy command や wrangler への言及がある → Workers
Pages のフローに直接入るなら /pages/new/provider/github を開けばいい。
なお preset は Build command と output を自動入力するだけのショートカットなので、手で入れるなら無くても困らない。Astro なら npm run build と dist だ。
2. Node のバージョンは明示しないと合わない
環境変数 NODE_VERSION を設定しないと、Cloudflare 側の既定 Node でビルドされる。依存パッケージが新しい Node を要求していると、ここで食い違う。
手元の package.json に engines を書いていなかったので、ロックファイルを見て必要な下限を確認した。テストランナーが >=22.12 を要求していたのに、デプロイ手順書には 20 と書いてあった。ローカルでは新しい Node で通っていたので気づかなかった。
"engines": { "node": ">=22.12" }
を足したうえで、Pages の環境変数にも NODE_VERSION = 22 を入れた。ビルドログに [email protected] と出れば効いている。
3. Web Analytics は自動注入が強制されることがある
Cloudflare Web Analytics には、スクリプトタグを自分で貼る方式と、プロキシが配信時に差し込む自動方式がある。ドメインが同じ Cloudflare アカウントのゾーンだと、自動方式しか提示されない。 トークンを取得する画面が出てこない。
これは、あらかじめレイアウトに beacon タグを書いておいた場合に問題になる。トークンが埋まらないまま残ると、無効なリクエストが毎ページ発生し、さらに自動注入と合わせて二重計上になる。タグは削除するしかない。
削除したあと、テストは「beacon タグが埋め込まれていないこと」を検証する形に反転させた。将来また誰かが手で足してしまうのを防ぐためだ。
代償として、Cloudflare のプロキシから離れると計測が止まる。「ホスティングを移しても計測が続くように手動タグにする」という当初の方針は、選択肢が無いので達成できなかった。
4. 独自ドメインのメールは無料で作れる(ただし受信専用)
問い合わせ用に新しいメールアドレスを作ろうとして、電話番号の制限で弾かれた。同じ番号で作れるアカウント数には上限がある。
ドメインを持っているなら Cloudflare Email Routing で足りる。[email protected] を作って既存の受信箱に転送でき、費用も電話番号も新規アカウントも要らない。公開されるのは転送先ではなく独自ドメイン側のアドレスだけだ。
制約は明確で、受信専用。送信は提供されないので、返信すると転送先のアドレスが相手に見える。公開されるわけではなく返信相手にだけ見える形なので、問い合わせ窓口としては実用上困らない。送信元も揃えたいなら外部の SMTP 中継が別途要る。
なお、プロキシ経由のゾーンでは Email Address Obfuscation が働き、HTML 中の mailto: が /cdn-cgi/l/email-protection に書き換えられる。curl で見ると mailto: が見つからず一瞬焦るが、スクレイパー対策として正しく動いている。ブラウザでは JS が復号する。
5. .dev の空き確認に whois は使えない
.dev レジストリは whois を公開していない。ローカルの whois は IANA にフォールバックし、TLD 自体の情報を返す。ドメインが空いているかどうかは判定できない。
$ whois example-not-registered.dev
% IANA WHOIS server
domain: DEV
organisation: Charleston Road Registry Inc.
RDAP を使う。
curl -sL -o /dev/null -w "%{http_code}" https://rdap.org/domain/example.dev
# 404 → 未登録 / 200 → 登録済み
-L を付け忘れると 302 が返って判定できない。また rdap.org は連続で叩くと 429 で止まる。
一次スクリーニングには DNS が速い。登録済みドメインはほぼ必ず NS を持つので、
dig +short NS example.dev @8.8.8.8
が空なら未委任=空きの可能性が高い、と当たりを付けてから RDAP で確定させる。レジストリ自身の RDAP エンドポイントは、登録済みドメインにも 404 を返すことがあったので当てにしなかった。
6. HTTP 200 を成功条件にしてはいけない
これが一番効いた。記事を push したあと、デプロイ完了を待つのにこう書いた。
until [ "$(curl -sL -o /dev/null -w '%{http_code}' https://example.com/posts/new-article/)" = "200" ]; do
sleep 10
done
即座に抜けた。そしてサイトは古いままだった。
Cloudflare Pages は存在しないパスに対してインデックスページを 200 で返す。つまり、まだ存在しない新記事の URL を叩いても 200 が返る。ステータスコードで判定する限り、この条件は常に最初から真になる。
正しくは中身で判定する。
until curl -sL https://example.com/posts/new-article/ | grep -q "本文にしか出ない文字列"; do
sleep 15
done
ダッシュボードを見に行くと、ビルドはまだ Queued ですらあった。「200 が返った=デプロイできた」と報告する直前だった。
ついでにもう一点。デプロイ直後に sitemap を確認したら新記事が載っていなかったが、これは CDN キャッシュだった。?cb=$RANDOM を付けて取り直したら入っていた。こちらも「見えない=無い」ではない。
検討したが採らなかった選択肢
Workers の Static Assets で配信する。 Astro の静的サイトは Workers でも動く。ただし wrangler.jsonc が必要になり、設定の考え方も手順書も変わる。静的サイトを置くだけなら Pages の方が素直なので採らなかった。統合が進んで Pages 側が畳まれたら再検討する。
問い合わせをフォームサービスに変える。 mailto: を晒さずに済むが、Cloudflare Pages には無料のフォームハンドラが無く、Worker を書くか外部サービスを使うことになる。「初期費用はドメイン代のみ」という制約があったので見送った。
送信も独自ドメインに揃える。 外部の SMTP 中継と Gmail の「他のアドレスから送信」を組み合わせれば可能だが、設定項目が増える割に、受信できていれば窓口としては成立する。後回しにした。
まとめ
- Framework preset は Step 2 にある。既定タブが Workers になっている点にも注意
- 自動注入が強制される構成では、手書きの beacon タグは削除する。残すと二重計上になる
- デプロイ完了の判定にステータスコードを使わない。 Pages は未知のパスにも 200 を返す