タグ: DNS

  • 勉強法(1) 1つ作り切ると、広く当たる

    勉強法(1) 1つ作り切ると、広く当たる

    勉強法の本を読んで、思い出したことがあります。

    いま自分が手を出せる範囲は、勉強して広げたものではありませんでした。このサイトを1つ作り切る過程で、必要になって触っただけです。

    この記事で扱うこと

    ポートフォリオサイトを1つ作るのに、結果として何を触ることになったか。その一覧と、それが「順番に勉強する」やり方とどう違ったかです。

    触ることになったもの

    やりたくて始めた領域は、ほとんどありません。進めようとしたら、そこで止まったから触ったという順番です。

    領域 止まった場面
    DNS ドメインを繋いだら、wwwの有無で別のサイトとして扱われた
    TLS 証明書が発行されるまで、何が起きているのか分からなかった
    共用サーバー 混むと500が返り、公開作業ごと止まった
    WAF コードを含む本文を投稿すると、403で弾かれた
    CMSの設計 本文をコードに書くか、CMSに置くかで運用が変わった
    キャッシュ 常に最新を取る指定のせいで、初回表示が1.7秒かかっていた
    アクセス解析 入れる前に、何を数えたいのかを決める必要があった
    検索エンジン 同じ中身が複数のURLで見える状態を、56件指摘された
    アクセシビリティ 画面では分かる区別が、読み上げでは同じに見えていた
    CSS設計 角丸の値を、どこまで段階として揃えるかで迷った

    この一覧は、事前に立てた計画ではありません。詰まった順に並んでいるだけです。

    どの順で何が起きたのかは、公開するまでに、何をどの順でやったか に並べました。この表の各行が、4つの段のどこで出てきたのかが分かります。

    順番に勉強していたら、たぶん届いていない

    もし「Web開発を学ぶ」という目的で本を選んでいたら、上の表の上半分には行き着かなかったと思います。

    DNSもTLSもWAFも、作る話ではなく、公開してから出てくる話だからです。教材の多くは動くところまでを扱っていて、そこから先は自分のドメインを持たないと出てきません。

    「必要になっていない知識」は、読んでも残らない

    実は、DNSの説明は以前にも読んだことがありました。読んだ記憶はあるのに、いざ設定画面を前にすると手が止まりました。

    困っていない状態で読んだものは、知識としては入っても、使う場所と結びついていません。結びつけるのは、詰まった経験のほうでした。

    1つ作り切ると、全部が繋がって出てくる

    もうひとつ効いたのは、これらがばらばらに出てこなかったことです。

    たとえば「表示を速くする」という話。キャッシュの置き場所を変えたら、公開作業の途中でCMSへ問い合わせに行くようになり、共用サーバーが混んでいると公開ごと止まるようになりました。速度の話をしていたつもりが、失敗したときにどうなるかを一緒に変えていたわけです。

    速度とサーバーの安定性と公開の仕組みは、別々に勉強していたら別々の知識のままでした。1つのものを動かし続けていると、つながっている場所のほうから見えてきます。

    範囲が決まっているから、終わりがある

    「全部やる」ではなく「このサイトを動かし続ける」が目的なので、どこまでやるかが自然に決まります。

    やらなかったことも多い。データベースの設計も、大量のアクセスをさばく話も、チームで開発する話も出てきていません。出てこなかったのは、必要にならなかったからです。

    作り切る、の意味

    ここで大事なのは「作る」ではなく「作り切る」のほうでした。

    手元で動かすところで止めていたら、公開に関わる領域は一切出てきません。公開しても放置していたら、運用で出てくる問題に当たりません。

    実際、上の表のうち半分以上は公開したあとに出てきたものです。作り終えてからのほうが、触る範囲は広がりました。

    どこで止めるか 出てこないもの
    手元で動いた ドメイン、証明書、サーバー、公開の仕組み
    公開した 検索エンジン、解析、速度、壊れたときの対処
    使い続けている —

    証拠が残るのも、同じ理由

    この一覧は、そのまま記事の一覧でもあります。詰まった場面を1つずつ書いてきた結果、60本を超えました。

    「勉強しました」と言うより、詰まった場所と直し方が並んでいるほうが、何ができるかは伝わります。狙って作った実績ではなく、作り切る過程の副産物です。

    今回の学び

    まず、触る範囲は、目的の広さではなく深さで決まるということ。「広く学ぶ」と決めて広がったのではなく、1つを最後まで進めたら、必要な順に出てきました。

    次に、困っていないときに読んだものは、使う場所と結びつかないこと。同じ説明でも、詰まった後に読むと入り方が違いました。順番としては、詰まるほうが先でした。

    そして、「作る」と「作り切る」では、出てくるものが違うこと。手元で動いた時点で止めていたら、触ったのは表の下半分だけだったはずです。公開して、使い続けて、初めて出てくるものがありました。

  • 所有権をどう証明するか

    所有権をどう証明するか

    「このサイトはあなたのものですね」を、Googleはどうやって確かめているのか。

    Search Consoleにサイトを登録すると、所有権の証明を求められます。方法は5つ提示されますが、それぞれ証明している中身が違います。同じ「所有権」という言葉でくくられているせいで、そこが見えにくくなっています。

    この記事で扱うこと

    5つの確認方法が何を根拠にしているか、DNSのTXTレコードだけが別格である理由、ドメインプロパティでDNS確認しか認められていない理由。そして実際に登録するときの注意点。

    5つの方法は、証明の強さが違う

    Search Consoleが提示する確認方法を、「何ができる人だと証明しているか」で並べ替えるとこうなります。

    方法 証明していること
    Googleアナリティクス そのGA4プロパティを編集できる
    Googleタグマネージャー そのGTMコンテナを編集できる
    HTMLタグ そのページのHTMLを書き換えられる
    HTMLファイル そのサーバーにファイルを置ける
    DNSレコード そのドメインそのものを支配している

    上の4つは、いずれも「サイトの中身に手が届く」ことしか示していません。制作を請け負っている人でも、CMSの編集権限を持っている人でも通せます。

    DNSだけが違います。DNSレコードを書き換えられるということは、そのドメインをどのサーバーに向けるかを決められるということです。極端に言えば、サイトの中身を丸ごと別の場所に差し替えられます。所有者でなければ持てない権限です。

    だからドメインプロパティはDNSしか認めない

    Search Consoleのドメインプロパティは、www の有無もサブドメインもすべてまとめて扱います。cms.kisaku.site のような、まだ存在しないサブドメインまで対象に含みます。まだ無いサブドメインにファイルは置けません。だから「ドメイン全体の持ち主である」ことを、上の階層で一度に証明する必要があります。それができるのはDNSだけです。

    TXTレコードとは何か

    DNSは本来、名前をIPアドレスに変えるための仕組みです。レコードには種類があります。

    種類 用途
    A / AAAA 名前をIPアドレスに変換する
    CNAME 名前を別の名前に転送する
    MX メールの宛先サーバーを示す
    NS このドメインを管理するサーバーを示す
    TXT 任意の文字列を置ける(用途は決まっていない)

    TXTだけが異質です。「好きな文字を書いておける場所」として定義されていて、何に使うかは決められていません。

    この自由さが、あとから来た用途に次々と使われました。所有権の証明もそのひとつです。SPF(メールの送信元認証)も、DKIMも、TXTレコードの上に成り立っています。

    実際に登録した内容
    ホスト名   (空欄。管理画面によっては @ と表示される)
    種別       TXT
    値         google-site-verification=(発行された文字列)
    TTL        既定のまま

    ホスト名を空欄にするのは、ドメインそのもの(kisaku.site)に対して置くという意味です。ここに www と入れると www.kisaku.site のレコードになり、ドメインプロパティの確認には使えません。

    なぜこれで証明になるのか

    仕組みは単純です。

    確認の流れ
    1. Googleが、あなた専用のランダムな文字列を発行する
    2. あなたが、その文字列を kisaku.site のTXTに登録する
    3. Googleが、世界中から見えるDNSに問い合わせる
    4. 発行した文字列が返ってくれば、確認完了

    ポイントは3番です。GoogleはあなたのサーバーやCMSを見に行っていません。公開されているDNSに問い合わせているだけです。

    そしてDNSの内容を書き換えられるのは、ドメインを管理している人だけです。第三者があなたのドメインのTXTレコードに勝手な値を入れることはできません。「書き換えられた」という事実そのものが証明になっているわけです。

    パスワードを渡さずに証明する

    この方式のうまさは、Googleに何の権限も渡していないことです。サーバーのログイン情報も、CMSの管理権限も渡していません。「あなたにしかできない操作を実際にやってみせる」だけで済んでいます。所有権の証明としては、これがいちばん筋の通った形です。

    反映を自分で確かめる

    DNSの変更は、登録した瞬間に世界中へ伝わるわけではありません。管理画面で保存したあと、実際に公開されるまでに時間がかかります。

    「確認」を押して失敗したとき、原因が登録内容の間違いなのかまだ反映されていないだけなのかは、画面からは分かりません。ここで待ち続けるか設定を疑うかを間違えると、時間を大きく無駄にします。

    自分で問い合わせれば、すぐに区別できます。

    TXTレコードを直接見る
    dig kisaku.site TXT +short
    
    "v=spf1 include:spf21.gmoserver.jp ~all"
    "google-site-verification=..."

    ブラウザだけでも確認できます。https://dns.google/resolve?name=kisaku.site&type=TXT を開くと、Googleのリゾルバが見ている内容がJSONで返ってきます。Googleが実際に何を見ているかを、Googleに聞くので、Search Consoleの判定と食い違いません。

    最初に確認したとき、まだ見えていなかった

    実際、私は1回目の「確認」で失敗しました。設定を疑う前にDNSへ直接問い合わせたところ、返ってきたのはSPFの1件だけで、確認用の文字列はまだ公開されていませんでした。失敗の原因が「間違い」ではなく「待ち」だと分かったので、設定をいじり回さずに済みました。

    保存できたのに、公開されない

    ここからは、実際に私がはまった話です。

    管理画面でTXTレコードを追加し、「保存しました」と表示されました。ところが何時間経っても、DNSに問い合わせると返ってくるのはSPFの1件だけ。TTLは600秒——10分です。10分待てば消えるはずのキャッシュを、何時間も疑っていました。

    原因は、編集していた管理画面が、そのドメインの答えを持っていなかったことでした。

    ドメインの管理画面は、1つとは限らない

    ドメインを買った会社と、そのドメインの問い合わせに実際に答えるサーバーは、別々にできます。どちらが答えるかを決めているのがNSレコードです。

    誰が答えているのかを聞く
    dig kisaku.site NS +short
    
    dns01.gmoserver.jp.
    dns02.gmoserver.jp.

    返ってきたのはレンタルサーバー側のDNSでした。ドメインを買った会社の管理画面ではありません。

    できること 今回の状態
    ドメイン登録側の管理画面 NSレコードを決める ここを編集していた
    NSが指しているDNS 実際の答えを返す こちらが本番だった

    権限のない側の画面に書いた設定は、エラーにならずに保存され、そして誰にも参照されません。これが厄介なところです。間違いを教えてくれる仕組みが、どこにもありません。

    「保存できた」は「効いている」ではない

    管理画面の保存成功は、その画面のデータベースに書けたことしか意味しません。それが世界から参照されるかどうかは別の話です。DNSに限らず、設定が複数の場所に分かれている仕組みでは同じことが起こります。

    先に、答えている場所を特定する

    この失敗を避ける手順は単純です。レコードを追加する前に、NSを引く。

    作業前に確認する
    # 誰が答えるか
    dig kisaku.site NS +short
    
    # ブラウザだけでも見られる
    https://dns.google/resolve?name=kisaku.site&type=NS

    返ってきたサーバー名で、どこの管理画面を開けばいいかが分かります。*.gmoserver.jp ならレンタルサーバーのコントロールパネル、*.vercel-dns.com ならVercel、*.cloudflare.com ならCloudflareです。

    既存のレコードが、目印になる

    正しい画面かどうかは、開いた瞬間に判断できます。すでに動いているレコードが並んでいる画面が本番です。今回で言えば、AレコードとSPFとサブドメインのCNAMEが揃っている画面。逆に、実際のサイトが動いているのに空っぽの一覧が表示されたら、そこは参照されていない画面です。

    既存のレコードを消さない

    DNSの編集で、いちばん怖いのはこれです。

    TXTレコードには、すでにSPF(メールの送信元認証)が入っていることがよくあります。これを消すと、送ったメールが迷惑メール扱いされるようになります。サイトの表示には何の影響もないので、しばらく気づきません。

    TXTは複数あってよい

    「TXTはひとつしか置けない」と誤解して、既存のSPFを上書きしてしまう事故が起きます。同じホスト名に複数のTXTレコードを共存させられます。追加するのは1行だけで、既存の行はそのまま残します。

    もっと危険なのはAレコードやCNAMEです。ここを触るとサイト全体が見えなくなります。DNSは、間違えたときの影響がサイト全体に及ぶ数少ない設定です。作業前に現在の内容を控えておくと、戻せます。

    今回の学び

    まず、同じ「所有権の証明」でも、証明している中身が違うということ。ファイルやHTMLタグは「中身に手が届く」ことしか示しません。DNSだけが「ドメインの持ち主である」ことを示します。選択肢が並んでいると同じものに見えますが、強さが違います。

    次に、TXTレコードは「用途が決まっていない」から使われていること。所有権の証明もSPFも、DNSの設計時には想定されていない用途です。汎用の置き場所をひとつ用意しておいたことが、後から効いています。

    そして、失敗したときは「間違い」か「待ち」かを先に切り分けること。DNSは反映に時間がかかる仕組みなので、この2つが同じ見た目になります。切り分けずに設定をいじると、正しかったものまで壊します。

    さらに、「保存できた」は「効いている」ではないこと。今回いちばん時間を取られたのは、参照されていない管理画面に正しい値を書き続けていたことでした。書き込む前にNSを引いて、答えている場所を先に特定する。これだけで防げます。

    最後に、影響範囲の広い設定ほど、触る前に控えを取ること。DNSは間違いがサイト全体に及びます。しかもメール側の不具合は、しばらく誰も気づきません。

  • wwwのあるなしで、別のサイトになる

    wwwのあるなしで、別のサイトになる

    kisaku.site と www.kisaku.site。どちらを開いても同じサイトが出ます。

    しかし機械から見ると、この2つは別のサイトです。今回、計測まわりの設定を3か所やって、そのすべてで「どちらを書くか」を選ばされました。選び方を間違えると、設定したのに数字がいつまでも0のままになります。

    この記事で扱うこと

    wwwありとwwwなしが別物として扱われる理由、apexドメインにCNAMEを置けないというDNSの制約、そして計測系の3つ(Search Console・GA4・OGP)で同じホスト名に揃える必要があること。

    まず用語を整理する

    呼び方 例 意味
    apexドメイン
    (ゾーンapex、ネイキッドドメイン)
    kisaku.site いちばん短い形。前に何も付かない
    サブドメイン www.kisaku.site
    cms.kisaku.site/wp/
    apexの前に名前を足したもの

    www は特別なものではありません。cms や blog と同じ、ただのサブドメイン名です。慣習として広まっているだけで、技術的な地位は他と変わりません。

    ここが出発点です。www が特別だと思っていると、なぜ別扱いされるのかが理解できません。

    なぜ「どちらか」に決める必要があるのか

    両方で同じ内容が見られる状態は、放っておくと3つの問題を生みます。

    困ること 何が起きるか
    検索エンジンから見て重複 同じ内容が2つのURLに存在する扱いになり、評価が分散する
    計測が2つに割れる 同じ人が両方を訪れると別の訪問者として数えられる
    Cookieが共有されない 片方でログインしても、もう片方では未ログイン扱いになりうる

    だから正規のアドレスを1つ決めて、もう片方はそこへ転送する。これが標準的なやり方です。このサイトは www.kisaku.site を正規にして、kisaku.site から転送しています。

    なぜ www 側を正規にしたのか

    好みではなく、DNSの制約が理由です。

    ドメインとサーバーを結びつけるとき、レコードの種類を選びます。

    種類 指す先 apexに置けるか
    A / AAAA IPアドレス 置ける
    CNAME 別のドメイン名 置けない

    apexドメインにはCNAMEを置けません。DNSの仕様上、CNAMEは「この名前は別名です」という宣言で、同じ名前に他のレコードを共存させられないためです。apexには NS や MX といったレコードが必ず存在するので、CNAMEと同居できません。

    これが効いてくるのは、VercelやNetlifyのようなサービスを使うときです。こうしたサービスは配信先のIPアドレスを固定していません。負荷や障害に応じて変わります。だからIPではなく名前で指してほしい——つまりCNAMEを求めます。

    ALIAS / ANAME という回避策

    「apexでもCNAMEのように振る舞う」独自レコードを用意しているDNSサービスもあります(ALIAS、ANAME、CNAMEフラットニングなど呼び方はさまざま)。ただしこれはDNSの標準ではなく、各社の独自拡張です。使えるかどうかは契約しているDNSサービス次第で、乗り換えの自由が下がります。

    まとめると、www 側はCNAMEを素直に置けるのに対し、apex側は置けません。制約の少ないほうを正規にして、制約のあるほうを転送元にする。これが www を正規にした理由です。

    逆に、自分でサーバーを持っていてIPアドレスが固定なら、apexを正規にする選択も普通にあります。どちらが優れているという話ではなく、構成によって答えが変わります。

    転送しても、統合はされない

    ここからが今回の本題です。

    転送を設定すると、人が kisaku.site を開いても www.kisaku.site に飛びます。見た目には1つのサイトです。

    しかし計測やSEOのツールは、転送を設定したからといって自動で統合してくれません。登録するときにどちらを書いたかで、拾えるものが変わります。

    設定した場所 登録した値 間違えるとどうなるか
    Search Console
    (URLプレフィックス)
    https://www.kisaku.site/ データが増えないまま。不具合との区別がつかない
    GA4 データストリーム https://www.kisaku.site 設定上の表示がずれる
    Next.js の metadataBase https://www.kisaku.site SNS共有時にOGP画像が出ない

    いちばん厄介なのは1行目です。間違っていても、エラーは出ません。ただ数字が0のままになります。「まだ検索に載っていないのだろう」と思って何週間も待ってしまう、という失敗が起こりえます。

    登録するのは「転送先」

    迷ったら、ブラウザのアドレスバーに最終的に表示されているほうを登録します。転送元ではありません。このサイトなら www 付きです。

    ドメインプロパティなら分かれない

    Search Consoleには、URLプレフィックスとは別にドメインプロパティという種類があります。こちらは www の有無も http / https の違いも区別せず、すべてまとめて扱います。

    URL プレフィックス ドメイン
    対象 入力したURLの配下だけ すべてのサブドメイン・両プロトコル
    確認方法 5種類から選べる DNSのTXTレコードのみ
    www の有無 別プロパティになる 区別しない

    「それなら最初からドメインで登録すればいい」と思えますが、そう単純でもありません。

    ドメインプロパティはすべてのサブドメインを含みます。このサイトには cms.kisaku.site/wp/ というWordPress用のサブドメインがあり、そちらは検索結果に出したくありません。ドメインプロパティにすると、見せたくない側の数字も一緒に入ってきます。

    また、確認方法がDNSのTXTレコード追加に限られます。DNSは設定を間違えるとサイト全体が見えなくなる場所です。手軽さでいえばHTMLタグを1行足すほうが安全です。

    両方持つという選択

    実際には、両方登録して並行して見ることもできます。URLプレフィックスで www 側を細かく見つつ、ドメインプロパティで全体を把握する。どちらか一方を選ばなければならない、という制約はありません。

    フロント側での指定

    Next.jsでは metadataBase に正規のURLを1か所書きます。

    layout.tsx(要点)
    const SITE_URL = "https://www.kisaku.site";
    
    export const metadata: Metadata = {
      metadataBase: new URL(SITE_URL),
      openGraph: {
        type: "website",
        siteName: "KiSAKU Portfolio",
        locale: "ja_JP",
        url: SITE_URL,
      },
    };

    これが必要なのは、SNSは相対パスの画像を読めないからです。OGP画像を /ogp.png のように書いても、共有先のサービスはそれをどのドメインの話か判断できません。metadataBase があると、Next.jsが絶対URLに変換して出力してくれます。

    ここで www なしを書くと、共有時の画像URLが転送を挟む形になります。転送に対応していないサービスでは画像が表示されません。

    確認の仕方

    設定が効いているかは、実際にアクセスして確かめます。

    転送されているかを見る
    curl -sI https://kisaku.site/ | head -5
    
    HTTP/2 308
    location: https://www.kisaku.site/

    308 は「恒久的な転送」です。検索エンジンに対して「こちらが正規です」と伝える意味を持ちます。302(一時的)だと、その意味が伝わりません。

    ブラウザで確認するときの落とし穴

    恒久的な転送はブラウザに強くキャッシュされます。設定を直したのに古い転送先へ飛び続ける、ということが起きます。以前、この勘違いで「本番が更新されていない」と思い込んだことがありました。実際にはURLの末尾に適当なクエリを付けるだけで、正しく転送されていることが確認できました。

    今回の学び

    まず、www は特別な存在ではないということ。cms と同じただのサブドメインです。ここを理解していないと、なぜ別サイト扱いされるのかが腑に落ちません。

    次に、どちらを正規にするかは好みではなく制約で決まること。apexにCNAMEを置けないというDNSの仕様が、そのまま結論を導きました。構成が変われば答えも変わります。

    そして、転送は人には効くが、ツールには効かないこと。転送を設定しても、Search ConsoleもGA4もOGPも、登録した値のとおりに動きます。3か所すべてで同じホスト名に揃える必要がありました。

    最後に、エラーが出ない間違いがいちばん高くつくこと。ホスト名を間違えても、どこにも警告は出ません。ただ数字が増えないだけです。壊れているのか、まだ何も起きていないのか、区別がつきません。沈黙する設定ほど、入れた直後に確かめる価値があります。

  • 本番公開でつまずいた11のこと — 仕組みから解説

    本番公開でつまずいた11のこと — 仕組みから解説

    前回、ローカルで作ったサイトを本番公開するまでの記録を書きました。今回はその中で出てきた仕組みを、初めて触る人向けに整理して解説します。

    作業記録だけだと「こう直した」で終わってしまい、次に似た場面で応用が効きません。なぜそうなるのかが分かっていれば、症状が違っても同じ考え方でたどり着けます。前回の記事を読んでいなくても分かるように書きますが、実例は前回のものを使います。

    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がいつ時点の情報かを教えてくれること。

    そして、原因が分からないときは推測を重ねるより、観測できる範囲を広げること。これがいちばんの学びでした。

  • ローカルのサイトを本番公開するまでにハマった話

    ローカルのサイトを本番公開するまでにハマった話

    ローカルのパソコンで作ったWebサイトを、世界中から見られる状態にする——いわゆる「本番公開」の作業をしました。手順書に沿えば1時間で終わるはずが、実際には丸一日かかっています。今回はその全過程を、途中で出てくる用語をひとつずつ説明しながら記録します。

    想定している読者は、HTML・CSS・JavaScriptは書けるけれど、サーバーやDNSやSSLはこれから、という方です。私自身がその立場で、今回いくつも初めてのことに出会いました。

    そもそも何をしようとしているのか

    今回のサイトはヘッドレス構成というものです。まずここから説明します。

    従来のWordPressサイトは、記事の管理も、見た目の表示も、ぜんぶWordPressがやります。訪問者のブラウザはWordPressに直接アクセスし、WordPressがHTMLを組み立てて返す。1つのソフトが全部を担当する形です。

    ヘッドレス構成では、この役割を2つに分けます。

    WordPress … 記事を書く・保存する(管理画面とデータベース)
    Next.js   … 見た目を作る・訪問者に表示する

    「ヘッドレス」は「頭がない」という意味で、この場合の”頭”は表示部分を指します。WordPressから表示機能を取り除き、データを保管して受け渡すことに専念させる、というイメージです。

    2つに分けると、置き場所も2つ必要になります。

    WordPress → お名前.comのレンタルサーバー(cms.kisaku.site)
    Next.js   → Vercel(kisaku.site)

    Vercelは、Next.jsで作ったサイトを公開するためのサービスです。GitHubにコードを置いておくと、それを自動で取ってきて、ビルドして、公開してくれます。

    ビルドという言葉も説明しておきます。開発中に書いているコードは人間が読みやすい形になっていて、そのままではブラウザで動きません。これを実際に動く形に変換する作業がビルドです。Next.jsの場合は、この段階でWordPressからデータを取ってきて、ページの形に組み立てるところまで行います。

    前回までの記事で、WordPress側の移行は終わっています。今回はNext.js側をVercelに載せ、最後に kisaku.site というドメインを新しいサイトに向けるところまでが目標です。

    デプロイ前に見つけた不具合

    Vercelに繋ぐ前に、設定ファイルを確認していて問題を見つけました。next.config.mjs の画像に関する設定です。

    {
      protocol: "https",
      hostname: "cms.kisaku.site",
      pathname: "/wp-content/uploads/**",   // ← ここが間違っていた
    }

    これが何の設定かというと、Next.jsの画像表示機能が使う「許可リスト」です。

    Next.jsには Image というコンポーネントがあり、画像を自動で最適化して表示してくれます。ただし、どこの画像でも無条件に扱うわけではありません。あらかじめ許可したホスト名とパスの画像しか受け付けない仕組みになっています。悪意のあるサイトの画像を勝手に処理させられるのを防ぐためです。

    ところが前回の移行作業で、このサイトのWordPressは /wp/ というフォルダの中にあることが判明していました。画像の実際のURLはこうなります。

    https://cms.kisaku.site/wp/wp-content/uploads/2026/07/image.png
                            ~~~~ ← これが入る

    許可リストのほうには /wp が入っていないので、一致しません。このままだとブログ記事の画像が全部表示されないことになります。

    直すのは1行です。

    pathname: "/wp/wp-content/uploads/**",

    この間違いは、私が以前「WordPressはルートフォルダにある」と誤解していた頃の設定が残っていたものでした。ひとつの誤解が、気付かないうちに別のファイルにも影響していたわけです。誤解が判明した時点で「この前提を使っている場所は他にないか」を探すべきでした。

    失敗1: 自動生成されるファイルをGitに入れていなかった

    最初のデプロイは20秒で失敗しました。原因はGraphQLの型定義ファイルがリポジトリに入っていなかったことです。

    順に説明します。GraphQLは、必要なデータを指定して取り出すための問い合わせ言語です。「記事のタイトルと本文と日付だけください」というように、欲しい項目を書いて送ると、その形で返ってきます。

    このプロジェクトでは GraphQL Codegen というツールを使っています。書いたGraphQLの問い合わせを読み取って、TypeScriptの型定義を自動生成してくれるものです。型定義があると、エディタが「このデータにはtitleという項目がありますよ」と教えてくれたり、存在しない項目を書いたときに警告してくれたりします。

    これは npm run codegen というコマンドで生成されます。自動生成されるものなので、私は .gitignore(Gitで管理しないファイルを指定するリスト)に入れていました。

    ところがVercelは、ビルド時に npm run build しか実行しません。codegenは走らないので、型定義ファイルが存在しないままビルドに入り、そこで落ちるという流れでした。

    対処は2つ考えられます。

    案A: ビルドコマンドを "npm run codegen && npm run build" に変える
    案B: 生成されたファイルをGitにコミットしてしまう

    一般論では案Aのほうがきれいです。自動生成物をGitに入れると、差分が読みにくくなり、複数人で開発するときに衝突の原因にもなります。

    それでも案Bを選びました。理由は、codegenがWordPressのGraphQLに接続してデータの構造を取りに行くからです。案Aにすると、ビルドが成功するかどうかが本番のWordPressが生きているかどうかに左右されます。

    案A: WordPressが落ちている → codegen失敗 → ビルド失敗 → デプロイできない
    案B: WordPressが落ちている → ビルドは成功する(表示するデータは無いが)

    ビルドは、なるべく外部のサービスに依存しないほうが壊れにくい。 そしてこの判断は、直後に正しかったと証明されます。まさにWordPressへの接続が失敗し続ける事態に陥り、案Aを選んでいたら原因調査のためのデプロイすらできなかったからです。

    教科書的な正解と、その場の正解は一致しないことがあります。大事なのはどちらを選ぶかより、選んだ理由を説明できることだと思っています。

    失敗2: ビルドは成功。でもデータが1件も出ない

    2回目のデプロイは43秒で成功し、サイトも表示されました。ところが中身を見ると、こうなっています。

    実績       : まだ実績が登録されていません。
    スキル     : まだスキルが登録されていません。
    ブログ     : まだ記事がありません。
    プロフィール: (WordPress管理画面で作成すると、ここに表示されます)

    WordPress側には投稿7件・実績4件・スキル14件・画像10件が入っています。それが1件も出てきません。

    そして厄介なのが、ビルドが「成功」と表示されていることでした。データ取得に失敗しているのに、エラーとして表面化していません。理由はコードにありました。

    const [profileResult, projectsResult, skillsResult, postsResult] =
      await Promise.allSettled([
        getProfilePage(), getProjects(), getSkills(), getPosts(),
      ]);
    
    const projects =
      projectsResult.status === "fulfilled" ? projectsResult.value : [];

    Promise.allSettled を説明します。JavaScriptで複数の非同期処理(時間のかかる処理)をまとめて実行する方法は主に2つあります。

    Promise.all        … 1つでも失敗したら、その時点で全体が失敗になる
    Promise.allSettled … 全部の結果を待って、成功・失敗を個別に返す(例外を投げない)

    allSettled を選んだのには理由がありました。たとえば「プロフィールページをまだ作っていない」という場合でも、それ以外の部分は表示されてほしいからです。1箇所の欠損でページ全体が真っ白になるのは避けたい。設計としては妥当な判断です。

    ただ今回は、4つ全部が失敗しました。それでも設計どおりに空の配列へ置き換えられ、「まだ登録されていません」という平和な画面が出てしまいます。

    想定していた「一部が欠ける」ではなく「全部が落ちる」が起きたとき、この防御は問題を隠す方向に働きました。

    ここから学べることを一般化すると、こうなります。「壊さない」と「気付ける」は別の要求であり、両立できる。 次の2行があれば、調査は最初の5分で終わっていました。

    if (projectsResult.status === "rejected") {
      console.error("[getProjects] failed:", projectsResult.reason);
    }

    エラーを飲み込む設計自体が悪いのではありません。飲み込んだ痕跡を残さなかったことが問題でした。

    失敗3: 犯人を推測して、2回外した

    接続先の設定を疑いました。GraphQLの接続先はこう書かれています。

    const endpoint =
      process.env.NEXT_PUBLIC_WORDPRESS_API_URL ?? "http://localhost:8080/graphql";

    process.env.○○ は環境変数を読み取る書き方です。環境変数は、コードの外側から値を渡す仕組みで、「開発中はローカルのWordPress、本番は本番のWordPress」のように環境ごとに変わる値を扱うのに使います。

    ?? はnull合体演算子といって、「左側が空だったら右側を使う」という意味です。つまりこのコードは、環境変数が読めなければ黙ってローカル用のURLにフォールバック(代替)するという動きをします。Vercel上にローカルのWordPressは存在しないので、全部失敗する——筋は通っています。

    Vercelの管理画面を見ると、環境変数が Sensitive(機密)という設定で登録されていました。これは値を秘匿するための設定で、登録後は管理画面からも中身が見えなくなります。

    ここで違和感を持ちました。この環境変数には NEXT_PUBLIC_ という接頭辞が付いています。Next.jsではこの接頭辞に特別な意味があり、付いた値はビルド時にコードへ直接埋め込まれます。

    【ビルド前】process.env.NEXT_PUBLIC_WORDPRESS_API_URL
    【ビルド後】"https://cms.kisaku.site/wp/graphql"   ← 値そのものに置き換わる

    埋め込まれたコードはブラウザに配信されるので、この値は原理的に秘密にできません。だから「公開してよい値にだけ NEXT_PUBLIC_ を付ける」というルールがあり、パスワードやAPIキーに付けてはいけないのもこのためです。

    公開前提の値をSensitiveにするのは矛盾している——そう考えて、いったん削除し、Sensitiveなしで登録し直し、再デプロイしました。

    結果は変化なし。 データは相変わらず空のままでした。

    この時点で、私は原因を2回外しています。しかも困ったことに、Sensitiveだと私自身も値を確認できないので、「URLが間違っているのか」「値が届いていないのか」の区別すらついていませんでした。

    観測できない対象を推測で潰しにいっても、当たったか外れたかが分からない。 やるべきだったのは、値を推測することではなく、実際に何が起きているかを表示させることでした。

    決め手: エラーを表示させるためだけのコードを書く

    方針を変えました。エラーが握りつぶされているなら、握りつぶさずにそのまま表示する経路を一時的に作ればいい、という発想です。

    診断専用のURLを1つ追加しました。アクセスすると、次の4つをまとめて返します。

    1. ビルド時に埋め込まれた環境変数の実際の値
    2. GraphQLクライアントが使っているURL
    3. 素のfetchで接続した結果(エラーの中身まで)
    4. アプリと同じ経路で実行した結果

    コードの要点はここです。

    try {
      const r = await fetch(endpoint, { method: "POST", /* ... */ });
      result.rawStatus = r.status;
    } catch (e) {
      result.rawError = {
        name: e.name,
        message: e.message,
        cause: String(e.cause),   // ← これが決定的だった
      };
    }

    e.cause を取っているのが重要な点です。Node.js(サーバー側でJavaScriptを動かす仕組み)の fetch は、通信の失敗をすべて同じメッセージに丸めてしまいます。

    TypeError: fetch failed

    名前解決に失敗しても、接続を拒否されても、証明書が一致しなくても、タイムアウトしても、これだけです。本当の原因は cause というプロパティの中に入っています。

    これはエラーのラッピング(包装)と呼ばれる設計です。低レベルの細かいエラーを、上位の統一されたエラーで包む。使う側が実装の違いを気にしなくて済む利点がありますが、原因を調べるときは包みを開ける必要があります。

    デプロイして、この診断URLを叩いた結果がこれでした。

    {
      "envRaw": "https://cms.kisaku.site/wp/graphql",
      "clientEndpoint": "https://cms.kisaku.site/wp/graphql",
      "rawError": {
        "message": "fetch failed",
        "cause": "Error [ERR_TLS_CERT_ALTNAME_INVALID]:
          Hostname/IP does not match certificate's altnames:
          Host: cms.kisaku.site is not in the cert's altnames:
          DNS:*.gmoserver.jp, DNS:gmoserver.jp"
      }
    }

    SSL証明書の不一致でした。 そして環境変数は正しく届いていたことも同時に分かりました。私が疑って作り直したSensitiveの件は、完全に的外れだったわけです。

    なぜブラウザでは繋がり、サーバーからは繋がらないのか

    この問題が見つけにくかった理由を説明します。ブラウザからは正常にアクセスできていたのです。鍵マークも付き、GraphQLもデータを返していました。

    理解するには、HTTPS通信の最初の手順を知る必要があります。

    HTTPSの「S」はSecure(安全)の意味で、通信を暗号化します。ただしその前に、相手が本物かどうかを確認する手続きがあります。これをTLSハンドシェイク(TLSは暗号化の規格名、ハンドシェイクは握手)といいます。

    1. こちらが「cms.kisaku.site と話したい」と伝える
    2. サーバーが、そのホスト名に対応する証明書を返す
    3. こちらが「証明書に書かれた名前」と「アクセス先の名前」が一致するか確認する
    4. 一致すれば暗号化通信を開始、しなければ接続を中断

    証明書は、身分証明書のようなものです。「このサーバーは確かに cms.kisaku.site です」と第三者機関が保証したデータで、そこには対象のホスト名が書かれています。

    今回失敗したのは3の段階でした。返ってきた証明書には *.gmoserver.jp としか書かれておらず、cms.kisaku.site が含まれていない。だからNode.jsは「名乗っている相手が違う」と判断して接続を打ち切りました。

    ここで重要なのは、この確認はデータを送る前に行われるということです。実際、Vercelのログには「外部への通信が1本もない」と表示されていました。リクエストを送って失敗したのではなく、送る前の握手で決裂していたのです。

    そしてヘッドレス構成では、この違いが致命的になります。

    従来のWordPress: ブラウザ ──────────────→ WordPress
    今回の構成      : ブラウザ → Next.js(Vercel) → WordPress
                                    ↑
                        ここの通信は、ブラウザからは見えない

    従来の構成なら、ブラウザで確認すれば十分でした。しかしヘッドレスではサーバーがサーバーを呼ぶ経路が主役です。私はずっとブラウザから確認して「サーバー側は正常だ」と判断していましたが、その経路は実際に使われる経路ではなかったのです。

    「自分の環境から動く」は「どこからでも動く」ではない。 検証は、実際に通信する経路で行わないと意味がありません。

    証明書はその後サーバー側で正しく適用され、コードを一切変えずにデータが表示されるようになりました。診断用のURLは役目を終えたので削除しています。

    危なかった話: DNSの設定画面を間違えるところだった

    サイトが動いたので、最後の工程です。kisaku.site というドメインを、新しいサイトに向けます。

    DNSを説明します。インターネット上のサーバーは、本来 216.198.79.1 のような数字(IPアドレス)で識別されます。しかし人間が覚えるのは大変なので、kisaku.site のような名前と数字を対応づける仕組みがあり、これがDNS(ドメインネームシステム)です。電話帳のようなものだと考えてください。

    Vercelにドメインを追加すると、設定すべき内容が表示されます。

    kisaku.site       A      216.198.79.1
    www.kisaku.site   CNAME  e2e46a9cb91ef14a.vercel-dns-017.com

    Aレコードは「この名前は、このIPアドレスです」という対応づけ。CNAMEレコードは「この名前は、別の名前の別名です」という指定です。

    さて、これをどこで設定するか。お名前.comの管理画面には「DNSレコード設定」という、いかにもそれらしいメニューがありました。開いてみると、入力欄が並んだ画面が出てきます。

    ところが、その画面の既存レコード一覧が空でした。いま動いているはずの設定が、1件も表示されません。

    ここで手を止めて、確認しました。

    kisaku.site のネームサーバー: dns01.gmoserver.jp / dns02.gmoserver.jp

    ネームサーバーは、そのドメインの答えを実際に持っているサーバーのことです。DNSには2つの階層があります。

    【上位】ネームサーバー … 「このドメインの答えは、どのサーバーが持っているか」
    【下位】レコード       … 「そのサーバーが持っている、実際の答え」

    つまり答えを持っているのはレンタルサーバー側で、私が開いていたのはお名前.comが提供する別のDNSサービスの画面でした。名前がそっくりなので紛らわしいのですが、まったく別物です。

    そして画面の下には「レコードの登録とあわせてネームサーバーも変更する」というチェックボックスがありました。もしこれを有効にして保存していたら、上位の階層が書き換わります。

    変更前: kisaku.site → gmoserverのDNS(3件のレコードが登録済み)
    変更後: kisaku.site → 別サービスのDNS(レコードは空)

    参照先が空のほうに切り替わるので、ぶら下がるすべての名前が一斉に消えます。

    kisaku.site      → 消える
    www.kisaku.site  → 消える
    cms.kisaku.site  → 消える  ← WordPressごと到達不能に

    cms が落ちればGraphQLも止まるので、新サイトもデータを取得できなくなります。「cms のレコードには絶対に触らない」と決めて作業していたのに、1行も触らないまま巻き添えにするところでした。

    「レコードを触らなければ安全」は成り立ちません。 上位を切り替えれば下位は丸ごと入れ替わります。作業前には、個々の行を見る前に「いま自分はどのゾーンを編集しようとしているのか」を確認する必要があります。

    気付けたのは、一覧が空だったことに違和感を持ったからでした。「あるはずのものが無い」は、たいてい自分が別の場所を見ている合図です。

    正しい場所で切り替える

    本来の作業場所は、レンタルサーバーのコントロールパネルでした。開くと、期待どおりのレコードが並んでいます。

    (なし) 標準 A     ← ルートドメイン(kisaku.site そのもの)
    www     標準 A
    ftp / pop / smtp / imap  標準 A   ← メール関連
    (なし) MX 10              ← メールの宛先
    (なし) TXT               ← 送信元の正当性を示す設定

    ルートドメインの行を編集すると「別サーバーを利用する」という選択肢があり、IPアドレスを指定できました。ここにVercelの 216.198.79.1 を入れます。

    画面には注意書きもありました。「ドメインのホスト名なしはAレコードのみ設定できます」。

    これはDNSの仕様上の制約です。理由を説明します。CNAMEは「この名前は別名です」という宣言なので、そのホスト名に他のレコードを共存させられません。ところがルートドメイン(www などが付かない、ドメインそのもの)には、DNSの仕組み上どうしても必要なレコードがあります。

    NS  … このドメインを管理するネームサーバー(必須)
    SOA … ゾーンの管理情報(必須)
    MX  … メールの宛先(メールを使うなら必要)

    これらと共存できないため、ルートドメインにCNAMEは置けないのです。VercelがルートにはAレコード、www にはCNAMEを指定してきたのは、この事情を踏まえたものでした。

    www にCNAMEを使うのは、接続先のIPが変わっても自動で追従できるからです。Aレコードで直接IPを書くと、相手のインフラ変更のたびに手で直す必要が出ます。

    失敗4: 末尾のドット1文字でエラー

    www の設定で、保存に失敗しました。

    「指定先」の入力内容が正しくありません。
    ・ドットの連続やドットとハイフンを連続することはできません。

    原因は末尾のドットでした。Vercelが表示していた値は e2e46a9cb91ef14a.vercel-dns-017.com. と、最後にドットが付いています。

    これはDNSの正式な記法で、「完全修飾名なので、これ以上ドメイン名を補完しない」 という意味を持ちます。ドットが無いと、システムによっては自動的に自分のドメイン名を後ろに付け足してしまうことがあり、それを防ぐための表記です。

    ところがこの入力欄は、ドットを受け付けない実装でした。外して入れ直したら通りました。

    情けないのは、私が事前に「入力欄によってはドットでエラーになるので、その場合は外してください」と自分で書いていたことです。注意点を知っていることと、実行時にそれを適用することは別だと痛感しました。

    反映を待つ ── 「変わらない」の正しい読み方

    DNSを変更した直後に確認すると、まだ古い値が返ってきます。ここで「保存できていないのでは」と疑って設定を触り直すと、事態が混乱します。

    DNSの答えにはTTL(Time To Live)という有効期限が付いています。今回は600秒でした。世界中のDNSサーバーは、一度取得した答えをこの秒数のあいだ保存(キャッシュ)し、その間は問い合わせ直しません。毎回問い合わせていたら世界中のDNSが処理しきれないためです。

    つまり変更直後に古い値が返るのは正常です。問題は「保存されていない」のか「キャッシュを見ているだけ」なのかをどう区別するかでした。

    ここで役に立ったのがTTLの残り秒数です。実際に観測した値を並べます。

    kisaku.site  A 133.130.64.184  ttl600   ← 取得したばかりのキャッシュ
    kisaku.site  A 133.130.64.184  ttl218   ← 382秒前に取得された答えを見ている
    kisaku.site  A 216.198.79.1    ttl600   ← 期限切れ後に取り直した結果

    TTLはキャッシュの残り寿命なので、600から218に減っていれば「これは382秒前の答えだ」と分かります。いま見ている情報がいつ時点のものかを読み取れるわけです。ゼロになるのを待って問い合わせ直せば、確実に現在の状態が得られます。

    なおブラウザで確認する場合は、パソコンとブラウザ自身のキャッシュも重なります。シークレットウィンドウで開くか、別のネットワークから見るのが手軽です。「反映されない」の多くは、どこかの層にキャッシュが挟まっているだけです。

    最終的にこうなりました。

    kisaku.site       → 216.198.79.1                          Vercel
    www.kisaku.site   → CNAME e2e46a9cb91ef14a.vercel-dns...  Vercel
    cms.kisaku.site   → 133.130.64.184                        無事
    

    守りたかった cms は最後まで無傷でした。https://www.kisaku.site/ で新サイトが表示され、SSL証明書も自動で発行されて、公開完了です。

    今回の学び

    技術的な収穫は、fetch failed の cause を必ず見ることでした。表面のメッセージだけ読んでいる限り、永遠に原因にたどり着けません。包まれたエラーは、包みを開けないと中身が分からない。

    進め方の話としては、推測から観測に切り替えるのが遅かったという反省です。今回、原因を2回外しました。どちらも「たぶんこれだろう」で設定をいじり、結果が変わらず、当たったのか外れたのかも判然としない、という時間の使い方です。

    最終的に効いたのは、エラーを表示させるためだけのコードを書くという、一見遠回りな手段でした。書くのに10分もかかっていません。推測で1回試すコストと、確実に答えを得るコストは、思っているほど差がない。

    そして、DNSのように間違えると広範囲が止まる作業では、操作の前に「いま自分はどの階層を見ているか」を確認する習慣が要る、ということ。レコードを1行も触らずにサイト全体を落としかけた今回は、それを実感する良い機会になりました。

    次回は、この作業を通して出てきた用語や仕組みを、もう少し体系立てて解説します。

    契約前に知っておきたかったこと

    ドメインとサーバーを同じ会社で契約すると、DNSの設定画面が2か所に増えます。

    ドメインを買った側の管理画面にもDNSの設定欄があり、レンタルサーバー側にもあります。そして実際に参照されるのは片方だけです。

    厄介なのは、参照されないほうに設定してもエラーが出ないことです。保存は成功し、画面上も正しく見えます。ただ、何も起きません。

    この1点を知っているだけで、丸一日の調査が要らなくなります。

    お名前.comでドメインを取得する