前回、ローカルで作ったサイトを本番公開するまでの記録を書きました。今回はその中で出てきた仕組みを、初めて触る人向けに整理して解説します。
作業記録だけだと「こう直した」で終わってしまい、次に似た場面で応用が効きません。なぜそうなるのかが分かっていれば、症状が違っても同じ考え方でたどり着けます。前回の記事を読んでいなくても分かるように書きますが、実例は前回のものを使います。
1. URLの「実体」と「転送先」は別物
まず、いちばん基本的で、いちばん引っかかりやすい話から。
ブラウザで https://example.com/admin/ を開いて、ページが表示されたとします。このとき「/admin/ にページがある」とは限りません。
Webサーバーにはリダイレクト(転送)という仕組みがあります。「そのURLは別の場所に移ったので、こちらへどうぞ」とブラウザに指示するものです。指示を受けたブラウザは自動的に転送先へアクセスし直します。
あなた: /admin/ をください
サーバー: それは /wp/admin/ にあります(301 転送)
ブラウザ: では /wp/admin/ をください(自動)
サーバー: どうぞ(200 OK)
↓
画面には「表示された」としか見えない
この自動追従は普段は便利です。しかし構成を調べる目的では邪魔になります。私は実際にこれで誤解し、「WordPressはルートフォルダにある」と思い込んだまま作業を進めてしまいました。
調べ方: 追従を切る
ブラウザの fetch には、転送を追いかけない設定があります。
await fetch('/admin/', { redirect: 'manual' }) // → status 0(転送された)
await fetch('/wp/admin/', { redirect: 'manual' }) // → status 200(実体がある)
redirect: 'manual' を指定すると、転送は「ステータス0」という特殊な状態で返ってきます。本物の200と、転送された先の200を区別できるわけです。コマンドラインの curl なら、追従オプション -L を付けないのが同じ意味になります。
覚えておくとよい原則: 「調べるための通信」と「使うための通信」では、適切な設定が違います。使うときは自動で追ってくれたほうが楽ですが、調べるときは加工されていない生の応答が必要です。
2. WordPressには基準となるURLが2つある
これはWordPressを使う人だけの話ですが、はまると長引くので書いておきます。
WordPressの「設定 → 一般」には、よく似た2つの項目があります。
WordPress アドレス (siteurl) … WordPress本体のファイルが置いてある場所
サイトアドレス (home) … 訪問者がアクセスするURL
普通のインストールでは両方が同じ値になるので、違いを意識することはありません。ところが「WordPressを専用フォルダに入れる」構成にすると、この2つが分かれます。
siteurl = https://cms.example.com/wp ← 本体は /wp/ の中
home = https://cms.example.com ← 見せるURLはすっきり
この構成では、トップに置いた小さな中継ファイルが /wp/ の中身を呼び出します。訪問者に見えるURLはきれいなまま、本体のファイル群はフォルダにまとめられる、という利点があります。
ここからが重要です。 WordPressのプラグインやテーマは、URLを組み立てるときに2つの関数を使い分けます。
home_url() → home を基準にする
site_url() → siteurl を基準にする
どちらを使うかは機能ごとの実装しだいです。実際、2つのAPIで判断が分かれていました。
REST API … home_url() 基準 → https://cms.example.com/wp-json/
GraphQL … site_url() 基準 → https://cms.example.com/wp/graphql
私は「REST APIがルートに出ているのだから、GraphQLもルートだろう」と考えました。同じサイトの、同じようなAPIなのだから揃っているはずだ、と。揃っていませんでした。
この構成を使うときは、「このURLは home と siteurl のどちらから作られるのか」を機能ごとに確認するのが正しい態度です。アップロード画像のURLも同じ理屈で影響を受けるので、画像が表示されない不具合にもつながります。
3. エラーは「包まれている」ことがある
これはWeb開発全般で役立つ話です。
サーバー側のJavaScript(Node.js)で通信に失敗すると、こういうエラーが出ます。
TypeError: fetch failed
これだけです。 情報がほぼありません。しかも、原因が何であってもこの同じメッセージになります。
名前解決に失敗した → fetch failed
接続を拒否された → fetch failed
証明書が一致しなかった → fetch failed
時間内に応答がなかった → fetch failed
本当の原因は cause というプロパティの中に入っています。
try {
await fetch(url);
} catch (e) {
console.error(e.message); // "fetch failed" ← 情報ゼロ
console.error(e.cause); // ← ここに本体がある
}
実際に開けてみたら、こう書かれていました。
Error [ERR_TLS_CERT_ALTNAME_INVALID]:
Hostname/IP does not match certificate's altnames:
Host: cms.example.com is not in the cert's altnames:
DNS:*.gmoserver.jp
一目で原因が分かります。証明書に書かれた名前が違う、と。
これはエラーのラッピング(包装)と呼ばれる設計です。低レベルの細かいエラーを、上位の統一されたエラーで包む。使う側が実装の違いを気にしなくて済む利点がありますが、原因を調べるときは包みを開ける必要があります。
cause はJavaScriptの標準仕様に入っている機能です。名前は環境によって違いますが(innerException、__cause__、Unwrap() など)、包む設計はどの言語にもあります。
覚えておくとよい原則: エラーメッセージが妙に短くて抽象的なときは、その下にもう一段あります。
4. HTTPSは「暗号化」の前に「本人確認」をしている
HTTPSの「S」はSecure(安全)で、通信を暗号化します。ただし、暗号化の前に相手が本物かを確認する手続きがあります。
これをTLSハンドシェイクといいます。TLSは暗号化の規格名、ハンドシェイクは握手のことです。
1. こちらが「example.com と話したい」と伝える
2. サーバーが、その名前に対応する証明書を返す
3. こちらが「証明書の名前」と「アクセス先の名前」が一致するか確認する
4. 一致すれば暗号化通信を開始、しなければ接続を中断
証明書は身分証明書のようなものです。「このサーバーは確かに example.com です」と第三者機関が保証したデータで、そこには対象のホスト名が書かれています。
ここで押さえておきたいのは、この確認はデータを送る前に行われるという点です。前回の失敗では、サーバーのログに「外部への通信が1本もない」と出ていました。リクエストを送って失敗したのではなく、送る前の握手で決裂していたのです。
証明書に書かれる名前のルール
証明書には複数の名前を書けます。*.example.com のようにワイルドカード(なんでも当てはまる記号)を使うこともできますが、ワイルドカードは1階層ぶんしかカバーしません。
*.example.com は…
shop.example.com → カバーする
a.b.example.com → カバーしない(階層が1つ多い)
example.com → カバーしない(サブドメイン部分が無い)
前回のケースでは、共用サーバーの既定の証明書 *.gmoserver.jp が返ってきていました。当然 cms.kisaku.site は含まれません。
5. 「自分の環境から動く」は「どこからでも動く」ではない
前回いちばん学びが大きかったのが、この点です。
証明書の問題が見つけにくかったのは、ブラウザからは正常にアクセスできていたからでした。鍵マークも付き、データも返ってくる。それでもサーバーからは失敗する。
なぜ結果が違うのか。通信する経路が違うからです。
従来のサイト: ブラウザ ──────────────→ サーバー
今回の構成 : ブラウザ → Next.js(Vercel) → WordPress
↑
ここの通信は、ブラウザからは見えない
従来のWordPressサイトなら、ブラウザがWordPressに直接アクセスするので、ブラウザで確認すれば十分でした。しかしヘッドレス構成ではサーバーがサーバーを呼ぶ経路が主役になります。
この経路は、ブラウザとは別のソフトが、別の場所から、別の設定で通信しています。証明書の検証も、ブラウザとNode.jsでは厳密さが違うことがあります。
覚えておくとよい原則: 検証は、実際に通信する経路で行わないと意味がありません。手元で動くことは、他の場所から動くことを保証しません。
6. エラーを握りつぶす設計の功罪
前回、データが取れていないのにビルドが「成功」し、画面も表示されてしまいました。原因はコードの書き方です。
const results = await Promise.allSettled([
getProjects(), getSkills(), getPosts(),
]);
const projects = results[0].status === "fulfilled" ? results[0].value : [];
複数の非同期処理をまとめる方法は主に2つあります。
Promise.all … 1つでも失敗したら、その時点で全体が失敗になる
Promise.allSettled … 全部の結果を待って、成功・失敗を個別に返す(例外を投げない)
allSettled を選んだ理由は「プロフィールページをまだ作っていない」といった部分的な欠損でページ全体が真っ白になるのを防ぐためでした。妥当な判断です。
ところが今回は4つ全部が失敗しました。それでも設計どおり空の配列に置き換えられ、「まだ登録されていません」という平和な画面が出ます。
想定していた「一部が欠ける」ではなく「全部が落ちる」が起きたとき、この防御は問題を隠す方向に働きました。
ただし、これは allSettled が悪いという話ではありません。次の2行があれば済んでいました。
if (results[0].status === "rejected") {
console.error("[getProjects] failed:", results[0].reason);
}
覚えておくとよい原則: 「壊さない」と「気付ける」は別の要求で、両立できます。エラーを飲み込むなら、飲み込んだ記録は残す。
7. 環境変数は「いつ」読まれるのか
環境変数は、コードの外側から値を渡す仕組みです。「開発中はローカル、本番は本番」のように環境ごとに変わる値を扱うのに使います。
Next.jsでは、変数名の頭に NEXT_PUBLIC_ を付けるかどうかで挙動が変わります。付けた場合、その値はビルド時にコードへ直接埋め込まれます。
【ビルド前】process.env.NEXT_PUBLIC_API_URL
【ビルド後】"https://cms.example.com/wp/graphql" ← 値そのものに置き換わる
埋め込まれたコードはブラウザに配信されるので、この値は原理的に秘密にできません。「公開してよい値にだけ付ける」というルールがあり、パスワードやAPIキーに付けてはいけないのもこのためです。
実務上の帰結が2つあります。
ひとつは、変更したらビルドし直さないと反映されないこと。管理画面で値を書き換えても、既存のデプロイには影響しません。
もうひとつは、フォールバックの書き方に注意が要ることです。
const endpoint = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8080/graphql";
?? は「左が空なら右を使う」という意味です。便利ですが、本番で設定漏れがあってもエラーにならず、黙ってローカル用のURLで動き続けます。前回まさにこれで、原因の特定が遅れました。
本番では明示的に落とす、という書き方も選択肢です。
const endpoint = process.env.NEXT_PUBLIC_API_URL;
if (!endpoint) throw new Error("NEXT_PUBLIC_API_URL is not set");
覚えておくとよい原則: 親切なフォールバックは、設定ミスを見えなくします。6節と同じ構図です。
8. DNSは2階層でできている
DNSは、example.com のような名前と 216.198.79.1 のような数字(IPアドレス)を対応づける仕組みです。インターネットの電話帳のようなものだと考えてください。
重要なのは、DNSが2つの階層でできていることです。
【上位】ネームサーバー … 「このドメインの答えは、どのサーバーが持っているか」
【下位】レコード … 「そのサーバーが持っている、実際の答え」
ドメインを買った会社の管理画面で「DNS設定」を開いても、そこが実際に使われているゾーンとは限りません。ネームサーバーが別の会社を向いていれば、答えを持っているのはそちらです。
前回、まさにこれで事故を起こしかけました。開いた画面のレコード一覧が空だったので、そこに新しく設定して「ネームサーバーも変更する」にチェックを入れていたら——
変更前: example.com → A社のDNS(3件のレコードが登録済み)
変更後: example.com → B社のDNS(レコードは空)
ぶら下がるすべての名前が一斉に消えます。 「このレコードには触らない」と決めていたサブドメインも、1行も触らないまま巻き添えになります。
覚えておくとよい原則: 「レコードを触らなければ安全」は成り立ちません。上位を切り替えれば下位は丸ごと入れ替わります。そして「あるはずのものが無い」は、たいてい自分が別の場所を見ている合図です。
9. ルートドメインにCNAMEが置けない理由
DNSのレコードには種類があります。よく使うのは次の2つです。
Aレコード … この名前は、このIPアドレスです
CNAMEレコード … この名前は、別の名前の別名です
Vercelのようなサービスは、たいていCNAMEを推奨します。接続先のIPが変わっても自動で追従できるからです。Aレコードで直接IPを書くと、相手のインフラ変更のたびに手で直す必要が出ます。
ところがルートドメイン(www などが付かない、ドメインそのもの)にはCNAMEを置けません。理由はこうです。
CNAMEは「この名前は別名です」という宣言なので、そのホスト名に他のレコードを共存させられません。「別名だ」と言っているのに他の情報も持っていたら矛盾するからです。
ところがルートドメインには、DNSの仕組み上どうしても必要なレコードがあります。
NS … このドメインを管理するネームサーバー(必須)
SOA … ゾーンの管理情報(必須)
MX … メールの宛先(メールを使うなら必要)
これらと共存できないため、置けないわけです。だからVercelはルートにはAレコード、www にはCNAMEを指定してきます。
「ルートを www に転送して、www を本体にする」という構成がよく使われるのも、この事情と関係があります。実体を柔軟に追従できる側に置いておく、という設計です。
10. 「反映されない」の9割はキャッシュ
DNSを変更した直後に確認すると、まだ古い値が返ってきます。ここで「保存できていないのでは」と設定を触り直すと、事態が混乱します。
DNSの答えにはTTL(Time To Live)という有効期限が付いています。世界中のDNSサーバーは、一度取得した答えをこの秒数のあいだ保存(キャッシュ)し、その間は問い合わせ直しません。毎回問い合わせていたら、世界中のDNSが処理しきれないからです。
つまり変更直後に古い値が返るのは正常です。問題は「保存されていない」のか「キャッシュを見ている」のかをどう区別するかでした。
役に立つのがTTLの残り秒数です。
example.com A 1.1.1.1 ttl600 ← 取得したばかりのキャッシュ
example.com A 1.1.1.1 ttl218 ← 382秒前に取得された答えを見ている
example.com A 2.2.2.2 ttl600 ← 期限切れ後に取り直した結果
TTLはキャッシュの残り寿命なので、600から218に減っていれば「これは382秒前の答えだ」と分かります。いま見ている情報がいつ時点のものかを読み取れるわけです。
さらに、パソコンとブラウザも独自にキャッシュを持っています。「サーバー側は切り替わっているのに自分のブラウザだけ古い」という状態はよく起こります。シークレットウィンドウで開くか、スマホの回線から見ると切り分けられます。
11. 原因が分からないときに何をするか
最後に、いちばん学びが大きかった進め方の話です。
前回、原因を2回外しました。「環境変数の設定が悪いのでは」「ファイアウォールが遮断しているのでは」——どれも筋は通っていますが、確かめずに設定をいじり、結果が変わらず、当たったのか外れたのかも判然としない、という時間を重ねました。
打開したのは、原因を推測するのをやめて、エラーを表示させるためだけのコードを書いたことでした。書くのに10分もかかっていません。そして一発で答えが出ました。
この経験から言えることが2つあります。
ひとつは、推測で1回試すコストと、確実に答えを得るコストは、思っているほど差がないということ。「調べるのは面倒だから、とりあえず怪しいところを直してみよう」と考えがちですが、外したときの時間と、外したかどうかすら分からない不確かさを考えると割に合いません。
もうひとつは、観測できない対象を推測で潰しても意味がないということ。値が見えない状態のまま「値が間違っているかも」と考えても、直したかどうかを確認する手段がありません。まずやるべきは見えるようにすることでした。
この記事で扱った話は、実は同じ構造を持っています。
cause を開ける … 隠れているエラーを見えるようにする
redirect: 'manual' で調べる … 隠れている転送を見えるようにする
TTLの残り秒数を読む … 情報の鮮度を見えるようにする
診断用のコードを書く … 内部の状態を見えるようにする
デバッグとは、推理することではなく、観測できる範囲を広げていく作業なのだと思います。 見えていないものについて考えている時間が長いと感じたら、それは考えを進めるタイミングではなく、見る手段を用意するタイミングです。
まとめ
次に似た場面で思い出せるよう、要点だけ並べておきます。
URLを調べるときは転送の自動追従を切ること。WordPressには home と siteurl の2つの基準があり、機能によってどちらを使うかが違うこと。fetch failed は cause を開けること。HTTPSは暗号化の前に本人確認をしていて、その段階で切れると通信は1バイトも発生しないこと。検証は実際に通信する経路で行うこと。エラーを飲み込むなら記録を残すこと。NEXT_PUBLIC_ の値はビルド時に埋め込まれるので、変更後は再ビルドが要ること。DNSは2階層で、上位を切り替えると下位は丸ごと入れ替わること。ルートドメインにCNAMEは置けないこと。「反映されない」はたいていキャッシュで、TTLがいつ時点の情報かを教えてくれること。
そして、原因が分からないときは推測を重ねるより、観測できる範囲を広げること。これがいちばんの学びでした。
