お問い合わせフォームのメールが本番で500を返していて、「あー、環境変数入れ忘れただけでしょ」と軽い気持ちで開いたんですよ。そこから半日溶けるとは思ってませんでした。
Astro + Cloudflare Pages で作ったコーポレートサイトの話です。最終的に直ったんですが、途中で何回も「これが原因だ!」と思い込んでは外し、思い込んでは外し、を繰り返していて。今振り返ると、いちばん最初に確認すべきだったことを最後まで確認していなかった、というオチでした。まあ、そういう話です。
まず、メールが飛ばない
ログを見たら親切に教えてくれていました。
"SMTP configuration is missing",
{ "smtpHost": false, "smtpUser": false, "smtpPassword": true, ... }
smtpPassword だけ true で、あとは全部 false。なるほど環境変数が入ってないんだな、と。wrangler.toml には全部書いてあるのに。
[vars]
SMTP_HOST = "..."
SMTP_USER = "..."
「書いてあるじゃん。なんで読まないの」と思いながらしばらく眺めていたんですが、これ、Cloudflare Pages だと wrangler.toml の [vars] がランタイムに反映されないんですね。Pagesの環境変数はダッシュボードで管理するのが正解。Workers のノリで wrangler.toml に書いていたのが間違いでした。
しかもプロジェクト名まで trade-point-corpo と trade-point-corpo-ssg で食い違っていて、そりゃ紐づかないわけです。ダッシュボードに変数を全部入れて、これは解決。
ここまではよかったんですよ。ここからが長い。
本番にした途端、ページが真っ白で [object Object]
メールは直った。で、本番にデプロイしたら今度はページが真っ白。画面に [object Object] の文字だけがぽつんと出てる状態です。コンテンツは何も描画されてない。
これ、何度か見たことあるやつでした。nodejs_compat を入れると、ランタイムが自分をNode.js環境だと勘違いして、本来Workers向けに動くべき部分が変な経路に落ちる。そうすると、どこかでオブジェクトがそのまま文字列化されて [object Object] だけが残る、みたいな壊れ方をするんですよね。過去に踏んで「あー、またこれか」となるやつ。
プレビューでは出てなかったんですよ。プレビューには互換性フラグを入れてなくて、本番にだけ nodejs_compat が入っていた。本番にした瞬間、環境変数だけじゃなくて、こういう描画まわりまで別のところが壊れた、という感じでした。プレビューで動いてたから油断してたんですけど、本番の境界でまとめて表面化したわけです。
直し方としては、compatibility_date を上げて AsyncIterable のネイティブ処理(fetch_iterable_type_support)を有効にする、でした。デフォルト有効化が 2026-02-19 で、プロジェクトの日付がそれより前だったので、そこを跨がせたら描画が戻りました。nodejs_compat 自体は worker-mailer が要求するので外せない。なら日付側で面倒を見る、という落とし所です。
WordPressのデータが出ない、そして 530
メールは直った。次はトップページにWordPressの記事が出ない。ログにこう出ます。
Error: Failed to fetch notices: 530
530。Cloudflare独自のエラーで、ざっくり「オリジンに到達できない」。
ここで僕はまた nodejs_compat を疑うわけです。だってさっきの真っ白事件が nodejs_compat 絡みだったから。「またお前か」と。「nodejs_compat を入れると fetch の挙動が変わって、Cloudflare配下のWordPressに繋がらないんだ」と、それっぽい理屈をこねて、互換性日付を上げたり下げたり、フラグを足したり引いたり。しばらくこれをやっていました。
でも今回は、これが完全に的外れだったんです。前科があるやつを疑ったら無実だった、みたいな。
あ、これ絶対違うわ、と思いながら同じことを繰り返している瞬間って、ありますよね。まさにそれでした。
ページごとのレンダリング方式を整理してみると、トップや物件ページはSSR(ランタイムでfetch)、お知らせ一覧はSSG(ビルド時にfetch)。「SSGのお知らせは出てる、SSRのトップは出てない」。じゃあランタイムのfetchが問題だ、SSG化すれば回避できる、と考えてSSGに寄せました。
そして、SSGにしたことで、はじめてビルドログにfetchのエラーが出るようになったんです。これが転機でした。
ビルドログに、犯人がいた
[wp.ts] WP_URL = "https://tradepoint.fun-garage.co.jp"
Error: getaddrinfo ENOTFOUND tradepoint.fun-garage.co.jp
ENOTFOUND。名前解決そのものが失敗している。
念のため手元やWebFetchからもそのドメインを叩いてみたら、どこからも ENOTFOUND。つまりこのホスト名、公開DNSにそもそも存在しないんですよ。
ランタイムでは 530、ビルドでは ENOTFOUND。エラーコードは違うけど、言ってることは同じでした。「そのドメイン、外から繋がらないよ」と。
ここでようやく「URL、合ってる…?」と疑い始めます。半日かけて、いちばん最初に確認すべきだったところに戻ってきたわけです。正しいWordPressのURLを確認したら、
https://wordpress.tradepoint.jp/
別のホストでした。設定していた tradepoint.fun-garage.co.jp は、移行だか廃止だかで、もう死んでいた。wordpress.tradepoint.jp を叩いたら一発で記事のJSONが返ってきました。生きてる。
PUBLIC_WP_API_URL を正しいホストに直して、再ビルド。あっさり通りました。
「なんでプレビューでは動いてたの?」
これがいちばん納得いってなかった点なんですが、答えはシンプルで。
プレビューと本番の差じゃなくて、時間の差だったんです。プレビューを検証していた頃は旧ホストがまだ生きていて、本番を確認する頃には既に消えていた。同じ(今は死んでる)URLを、たまたま生きてた時に踏んだか、死んだ後に踏んだか、というだけの違いでした。
「プレビューでは動いた」という記憶が、ずっと僕に「コードか設定の差だ」と思い込ませていたんですね。実際にはインフラ側がいつのまにか変わっていた。これは盲点でした。
おまけ:SSRに戻したら、ゴミ箱の記事が消えない
最終的に「記事を更新したら即反映したい」という要件でSSRに戻したんですが、今度は記事をゴミ箱に入れてリロードしても一覧に残る。でも詳細ページは404になる。詳細だけ正しく消えるのが地味に気持ち悪い挙動で。
これは wp.ts にモジュールレベルのメモリキャッシュが仕込んであったせいでした。一覧系だけキャッシュして、詳細系はしてなかった。Worker isolate が生きてる間ずっと古い一覧を返すので、一覧にはゴミ箱の記事が残り、詳細は毎回fetchするから404、という。SSGの頃は「ビルド時に1回」だから無害だったキャッシュが、SSRにした瞬間に鮮度を殺す側に回っていました。キャッシュを消して解決。
今思っていること
530 とか ENOTFOUND を見た瞬間に、コードでも互換性フラグでもなく「そのホスト、本当に生きてる?」を最初に確認していれば、半日は溶けなかったなと思います。
ただ、nodejs_compat を疑ってあれこれ触った時間が完全に無駄だったかというと、おかげで互換性日付やPagesの環境変数の仕様にはやたら詳しくなったので、まあそれはそれで。
いちばんの教訓は、「プレビューでは動いていた」という記憶を疑えなかったことかもしれません。コードは変えてなくても、外の世界は勝手に変わる。そういうことを思いながら直していました。