タグ: SEO

  • 使っていないものは、書かない

    使っていないものは、書かない

    広告を入れて収益にできないか、という話から始まりました。

    調べていて手が止まったのは、広告そのものではありません。プライバシーポリシーのページが、このサイトに無かったことです。

    この記事で扱うこと

    広告を入れるかどうかに関係なく、すでに必要だったという話。テンプレートをそのまま貼らなかった理由。そして、置く場所と管理の仕方をどう決めたかです。

    広告の話から入ったが、広告の問題ではなかった

    広告配信サービスの規約を読むと、Cookieを使うことと、利用者が無効にできることを、プライバシーポリシーに明記するよう求められています。

    そこで自分のサイトを見て、ページ自体が無いことに気づきました。

    ただ、よく考えると広告の話が出る前から必要でした。

    すでにやっていたこと 本来必要だったこと
    アクセス解析でCookieを使う 何を集めているか、断る方法の説明
    問い合わせフォームを作った 受け取った情報の使い道の説明
    読了状態をブラウザに保存 保存していること自体の説明

    広告は4つめにすぎません。

    新しいことを検討して、古い抜けが見つかる

    今回は「広告を入れるには何が要るか」を調べたことで、すでに必要だったのに無かったものが出てきました。

    抜けは、抜けているという理由では見つかりません。誰も困っていないからです。別のことを始めようとしたときに、ついでに見つかることのほうが多いと思います。

    テンプレートをそのまま貼らなかった

    この手の文書には雛形がたくさんあります。貼れば5分で終わります。

    ただ、実際に読んでみると使っていないサービスの記述が入っていました。広告配信サービスを利用しています、という一文もそのひとつです。

    これを貼ると、使っていないのに使っていると書いてある状態になります。

    書いてあることは、確かめられる

    プライバシーポリシーは、読む人が確かめるための文書です。書いてあるとおりかどうかは、ページのソースを見れば分かります。

    実態と違うことが書いてあれば、それは説明ではなく、ただの飾りです。飾りを置くくらいなら、無いほうがまだ正直かもしれません。

    そこで、現時点では広告配信を行っていないと明記しました。始めたら、そのとき追記します。

    見えないことをしているなら、書く

    もうひとつ迷ったのが、読了状態の保存です。

    これはブラウザの中だけに記録していて、こちらのサーバーには届きません。個人を特定する情報でもありません。書かなくても問題にはならない種類のものです。

    それでも書きました。理由は、利用者から見えないからです。

    記事一覧に「読了済み」と出るのは、どこかに記録されているからです。読む人がそれに気づいたとき、どこに何が保存されているのか分かる場所があるほうがいい。法的に必要かどうかと、説明したほうがいいかどうかは、別の基準です。

    置き場所と、管理の仕方

    フッターの最下部に置いた

    ナビゲーションには入れませんでした。普段見たいものではないので、並べると他の項目の邪魔になります。

    一方で、探しに来た人が見つけられないのは困ります。フッターの最下部は、この種の文書が置かれる場所として広く定着しています。決まった場所にあることが、探す側にとっての手がかりになります。

    CMSから取る形にした

    文面をコードに直接書くこともできました。やめた理由は、更新のされ方です。

    コードに書いた場合
    サービスを1つ増やす
       ↓
    ファイルを編集して、ビルドして、デプロイする
       ↓
    面倒なので後回しになる
       ↓
    実態と食い違ったまま残る

    この文書は正しさが時間とともに失われる種類のものです。使っているサービスが変われば、その日から内容がずれます。

    用語集と同じく、管理画面から直せる形にしました。手順が軽いほど、実際に更新されます。

    今回の学び

    まず、新しいことを始める前に、足元の抜けが出てくること。広告の検討をしなければ、しばらく気づかないままでした。何かを増やそうとするときは、いま動いているものの前提も一緒に点検する機会だと思います。

    次に、テンプレートは、書いてある内容が自分に当てはまるか確かめてから使うこと。雛形は考える手間を省いてくれますが、確かめる手間まで省くと、事実と違う文書ができあがります。

    そして、法的に必要かどうかと、説明したほうがいいかどうかは別だということ。見えないところで何かをしているなら、書いておく理由はそれだけで十分です。

    最後に、更新されない場所に置くと、正しさは時間とともに失われること。書いた瞬間だけ正しい文書は、しばらく経つと嘘になります。直しやすい場所に置くことは、内容を正しく保つための設計の一部です。

  • 順番で決まる名前は、いつか壊れる

    順番で決まる名前は、いつか壊れる

    用語集の102語と、記事のあいだにリンクを張ろうとしました。

    ところが張る前の下調べで、そのリンクが最初から壊れる仕組みになっていることが分かりました。順番を変えただけで壊れます。

    この記事で扱うこと

    並び順から自動でIDを作ることの危うさ、公開前の記事へリンクしてしまっていた話、そして「直したつもり」で終わらせないための確認方法です。

    用語ページの中身を、直接指せるようにしてある

    用語集は分野ごとに1ページで、1ページに10〜18語ほど入っています。個々の語は見出しに id を振ってあり、こう書けば特定の語へ直接飛べます。

    語を直接指す
    /glossary/security/#waf

    記事から用語へリンクするには、この id が要ります。そこで中身を確認しました。

    半分以上が、意味のない名前だった

    IDは用語名から機械的に作っていました。英数字だけを残し、それ以外を捨てる方式です。

    用語 できたID
    WAF waf
    DNS dns
    ネームサーバー term
    名前解決 term-2
    サブドメイン term-3

    日本語の語は、英数字を残すと何も残りません。空になったものには、衝突しないように連番を振っていました。

    102語のうち55語がこの状態でした。

    なぜ、これが壊れるのか

    問題は読みにくさではありません。この連番が「何番目に出てきたか」で決まることです。

    用語を1語追加すると
    追加前                    追加後
    term    ネームサーバー     term    ネームサーバー
    term-2  名前解決           term-2  【ここに1語追加】  ← 割り込む
    term-3  サブドメイン       term-3  名前解決           ← ずれた
                              term-4  サブドメイン       ← ずれた

    用語を1語足すと、それ以降のIDが全部1つずつずれます。

    そして、ずれてもエラーは出ません。ページは正常に表示されます。リンクを踏んだ人が、ページの先頭に着地して「あれ?」と思うだけです。

    外から指される名前は、中の都合で変えてはいけない

    IDは、他の場所から指すための名前です。指す側は、指される側の並び順を知りません。

    用語集を充実させるほど、つまり良い方向に手を入れるほど、リンクが壊れていく。更新すると壊れる仕組みを先に作ってしまっていました。

    手で書いて、増えないようにした

    自動生成をやめ、102語すべてに固定のIDを手で割り当てました。

    対応表を1枚用意する
    "ネームサーバー": "nameserver",
    "名前解決":       "name-resolution",
    "サブドメイン":   "subdomain",

    手で書くのは面倒に見えますが、面倒なのは最初の1回だけです。追加する語は1行足すだけで、既存のIDは動きません。

    あわせて、対応表に無い語が来たら止まるようにしました。

    黙って埋めるより、止まったほうがいい

    未登録の語に自動で連番を振る作りだと、また同じ問題が静かに戻ってきます。止まれば、その場で1行足すだけで済みます。

    「気を付ける」で運用するより、間違えられない形にしておくほうが確実です。

    もうひとつ、公開前の記事へリンクしていた

    用語集には「関連記事」を載せています。ここも確認したところ、22件のうち8件が、まだ公開していない記事を指していました。

    用語集のページは公開済みです。つまり読者から見ると、リンクを踏むと404になります。

    原因は単純で、記事を書く前提でリンクを先に書いていたためです。

    そこで、公開済みの記事の一覧を持たせ、そこに載っていないリンクは出力しないようにしました。記事を公開したときに一覧へ1行足せば、リンクも一緒に出てきます。

    ついでに、リンクの文字をスラッグから記事タイトルに変えました。apex-vs-www と書かれても、押す前に中身が分かりません。

    直したあと、本当に直ったかを確かめる

    ここが今回いちばん大事な部分です。

    本番のページを書き換える必要がありましたが、「更新しました」という応答は、内容が正しいことを意味しません。途中で一部だけ置き換わっていても、成功として返ってきます。

    そこで、手元で作ったHTMLと、本番に入ったHTMLを数値に変換して突き合わせました。

    確認の流れ
    手元で組み立てたHTML   → 数値に変換 → A
    本番に入ったHTML       → 数値に変換 → B
    
    A と B が一致 → 1文字も違わない
    

    文章を1文字ずつ見比べるのは現実的ではありません。全体を1つの数値に潰してしまえば、一致するかどうかだけを見れば済みます。

    8ページすべてで一致しました。さらに、書き換えたあとで全リンクの飛び先が実在するかも機械的に確認しています。

    確認したこと 件数 壊れ
    記事 → 用語集のリンク 230 0
    用語集 → 記事のリンク 38 0
    下書きのまま残っているか 10件すべて —
    確認まで自動にしておく

    268本のリンクを手で踏んで確かめることはできません。目視で確認できない量になった時点で、確認する仕組みのほうを用意する必要があります。

    「たぶん大丈夫」と「1件も壊れていないことを確かめた」は、別のことです。

    今回の学び

    まず、外から指される名前は、中の都合で変わってはいけないこと。並び順・件数・登録した順番といった内部の事情でIDが決まると、中を整理するたびに外が壊れます。名前は約束なので、勝手に変えられない形にしておくべきです。

    次に、自動生成は、入力の性質を確かめてから使うこと。英数字を残す処理そのものは間違っていません。日本語の用語に対して使ったことが間違いでした。半分以上で空になると分かっていれば、最初から別の方法を選んでいたはずです。

    そして、足りないときは黙って埋めずに止めること。埋めてくれる親切な仕組みは、間違いも一緒に埋めます。止まれば気づけます。

    最後に、直したことと、直ったことは違うこと。更新が成功したという応答は、内容が正しい証明にはなりません。手元の正解と本番を突き合わせて初めて、直ったと言えます。この確認自体が数秒で終わるなら、やらない理由はありません。

  • 1語1ページにしなかった理由

    1語1ページにしなかった理由

    用語集を作ることになりました。記事に出てくる専門用語を、初学者向けに解説するページです。

    問題は入れ物の選び方でした。1語につき1ページか、まとめて1ページか。この判断に、思ったより多くの要素が絡んでいました。

    この記事で扱うこと

    同じ内容でも置き方で使いやすさが変わるという話と、途中で前提が変わったときにどう判断を修正したか。そして「正攻法が使えないと分かったとき」の落としどころの決め方です。

    最初の判断は「1枚にまとめる」だった

    理由は運用でした。用語は50個、100個と増えていくものです。1語ずつ投稿画面を開いて登録する形にすると、そのうち止まります。

    1枚のページに追記していく形なら、思いついたときに1行足すだけで済みます。続けられる形を優先するという判断でした。

    やりたいことが1つ増えて、前提が変わった

    次に出てきたのが「初学者にも分かる解説を加えたい」という考えでした。

    これを入れると、1語あたりの分量が変わります。定義1行では足りず、なぜそれが存在するのか、具体例、つまずきやすい点まで書くことになります。1語400〜800字といったところです。

    単純な掛け算
    102語 × 600字 ≒ 61,000字

    1枚にまとめると6万字を超えます。目当ての語にたどり着くまでスクロールし続けることになり、用語集として使えません。

    前提が変わったら、結論も変わる

    「1枚がいい」という判断は、1語が短いという前提の上に立っていました。深く書くほど、その前提は崩れます。最初の結論に固執せず、条件が変わった時点で見直す必要がありました。

    1語1ページを検討した

    用語ごとにURLを持たせる案には、はっきりした利点があります。

    「WAF とは」で検索した人に、その1語のページを届けられます。1枚にまとめると、ページ全体で1つのURLなので、個々の語では上位に出にくくなります。

    記事から特定の語へ直接リンクすることもできます。

    ところが、逆効果になる可能性があった

    調べていて引っかかったのが、内容の薄いページを大量に作ることのリスクです。

    1語400字のページが100枚あるサイトは、検索エンジンから「中身の薄いページが多いサイト」と見なされる可能性があります。個々の語で上位を狙うつもりが、サイト全体の評価を下げかねません。

    1ページあたりの分量 評価
    1語1ページ 約400〜800字 薄いと判断される可能性
    分野別にまとめる 約7,000〜9,000字 読み物として成立する

    「個別ページのほうが検索に強い」という一般論が、この規模では当てはまらないことが分かりました。

    もうひとつ、実装上の壁があった

    1語1ページにするには、WordPressに専用の投稿タイプを追加する必要があります。ところが調べると、既存の「実績」「スキル」はサーバー上のテーマファイルに直接書かれていました。

    つまり追加するには、本番のPHPファイルを編集することになります。

    本番のPHPを、ブラウザの入力欄から編集しない

    PHPは文法エラーが1文字あるだけで動かなくなります。そしてWordPressの管理画面にも入れなくなります。その状態から戻すにはFTPが必要です。

    「たぶん大丈夫」で触る場所ではありません。復旧手段を確保してから行うべき作業です。

    分野別に8ページ、という落としどころ

    結果として選んだのは、分野ごとに1ページ、全8ページという形です。

    構成
    /glossary/            分野の一覧
    /glossary/infra/      インフラ・ネットワーク  14語
    /glossary/security/   サーバー・セキュリティ  12語
    ...
    

    各ページの先頭に語の索引を置き、それぞれの語には id を振りました。/glossary/security/#waf のように、外部から特定の語を直接指せます。

    1語1ページの利点である「直接リンクできる」は、これでほぼ回収できます。失うのは「その1語だけで検索結果に出る」ことですが、前述の通り、この規模では狙わないほうが安全でした。

    一覧が、それ自体で使える

    索引を置いたことで、ページを開いてすぐ全体を見渡せます。1枚にまとめる案の良さだった「一望できる」は、分野ごとに残っています。

    実装で、新しいものを作らずに済んだ

    もうひとつ判断があります。用語集のページをどう取得するか、です。

    新しい問い合わせを追加すると、このプロジェクトでは型を作り直す必要があります。作り直しにはCMSへの接続が要るので、作業できる環境が限られます。

    ところが既存の問い合わせを見ると、こう書かれていました。

    既にあったもの
    getProfilePage(uri: string = "/profile/")

    URIを引数に取る作りになっていました。プロフィール専用に見えて、実際には「URIから固定ページを引く」という汎用の処理です。

    名前だけを分けて公開し、用語集のURIを渡すことで、問い合わせを1つも増やさずに実装できました。

    名前が用途を狭く見せていた

    getProfilePage という名前のせいで、プロフィール専用のものに見えていました。中身は汎用なのに、名前が可能性を隠していたわけです。

    新しく作る前に、既にあるものが本当に使えないかを見る価値はあります。特に、名前が具体的すぎるものほど確認する意味があります。

    今回の学び

    まず、前提が変わったら結論を見直すこと。「1枚にまとめる」は、1語が短いという前提の上での正解でした。解説を厚くすると決めた時点で、その前提は崩れています。最初の判断を守ることが目的ではありません。

    次に、一般論が自分の規模で成立するかを確かめること。「個別ページのほうが検索に強い」は間違いではありませんが、1ページあたりの中身が薄ければ逆に働きます。条件付きの正解を、無条件の正解として受け取らないことです。

    そして、正攻法が使えないときの落としどころを、妥協と考えないこと。今回は本番のPHPを触れないという制約がありましたが、その制約が無くても分野別を選んだと思います。制約から入って、結果的により良い形にたどり着くことはあります。

    最後に、新しく作る前に、既にあるものを見ること。名前が具体的すぎるだけで、中身は汎用ということがあります。1つ増やさずに済むなら、それだけ壊れる場所が減ります。

  • 推測させるのをやめる

    推測させるのをやめる

    検索エンジンに、推測させるのをやめる。

    今回入れたのは3つです。sitemap.xml、robots.txt、そして構造化データ。バラバラの施策に見えますが、やっていることは同じです。これまで相手に推測させていたことを、こちらから明示的に伝える。それだけです。

    この記事で扱うこと

    3つがそれぞれ何を伝えているか、順位と発見速度は別物だという話、そして実装時に踏んだ「一覧用のクエリを流用してはいけなかった」という落とし穴。SEOの小手先ではなく、仕組みとして何が起きているかの話です。

    3つとも「伝える」ための仕組み

    先に全体像を置きます。

    伝えていること 無いとどうなるか
    sitemap.xml どんなページが存在するか リンクをたどって発見されるまで待つ
    robots.txt どこを見てよいか / 地図の場所 全部を手探りで巡回される
    構造化データ そのページが何なのか HTMLの見た目から推測される

    右の列に共通しているのは、「無くても動くが、相手の推測に任せることになる」という点です。推測は当たることもあれば外れることもあり、こちらからは制御できません。

    sitemap.xml — 順位ではなく、見つかるまでの時間

    まず誤解を解いておきます。sitemapを置いても、検索順位は上がりません。

    これは「見つけてもらうまでの時間」を短くするものです。検索エンジンは通常、リンクをたどってページを見つけます。外部からのリンクがまだ少ない新しいサイトでは、存在しているのに気づかれない期間が生まれます。sitemapは、そのたどる作業を省いて一覧を直接渡す仕組みです。

    Next.jsのApp Routerでは、app/sitemap.ts に既定の関数を書くだけで /sitemap.xml が生成されます。XMLを自分で組み立てる必要はありません。

    app/sitemap.ts(要点)
    export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
      const staticEntries = [
        { url: `${SITE_URL}/`,     changeFrequency: "weekly",  priority: 1   },
        { url: `${SITE_URL}/blog`, changeFrequency: "weekly",  priority: 0.8 },
        // ...
      ];
    
      const posts = await getPostsForSitemap();
      // ...
    }
    priority は「相対的な」重要度

    0から1の値ですが、これは検索エンジンに対する評価の申告ではありません。サイト内での相対的な優先度です。全部を1.0にすると差が無くなり、指定していないのと同じになります。

    20本の壁

    実装中に気づいた問題です。ここが今回いちばんの学びでした。

    記事一覧を取るための関数は、すでに用意してありました。getPosts() です。sitemapもこれを使えば済む——と思ったのですが、中身を見て手が止まりました。

    一覧用のクエリ
    query GetPosts {
      posts(first: 20) {   ← 20本で打ち切っている
        ...
      }
    }

    一覧ページに20本並べる、という表示上の都合でこうなっていました。これをsitemapに流用すると、21本目以降は「存在するのに検索エンジンへ知らせない記事」になります。

    気づかないまま進む種類の不具合

    この漏れは、エラーになりません。sitemapは正常に生成され、中身も正しく見えます。足りないことに気づく手がかりが、どこにもありません。公開記事がちょうど20本だったので、次の1本を公開した瞬間から静かに始まるところでした。

    教訓は単純です。「一覧に何本並べるか」と「検索エンジンに何本知らせるか」は別の話でした。同じ「記事一覧」という名前がついているせいで、同じものに見えていただけです。

    sitemap専用に、全件を取る関数を分けました。

    型生成への依存を、持ち込まない

    ここでもうひとつ判断が要りました。

    このプロジェクトのGraphQLクエリは、graphql-codegen が生成した型付きのドキュメントを経由しています。型が付くのは大きな利点ですが、制約がついてきます。クエリを1文字でも変えたら、型を作り直さなければなりません。そして作り直しにはCMSへ接続してスキーマを読む必要があります。

    得られるもの 引き換えに背負うもの
    型付きクエリ 取り違えを防げる CMSに繋がる環境でしか変更できない
    素の文字列 どこでも書ける 型は自分で書く

    sitemapが必要としているのは slug と modified の2つだけです。2つしか無いものを、取り違えようがありません。ここだけ素の文字列で問い合わせ、型は手で書くことにしました。

    仕組みの利点は、全部に適用しなくてよい

    型生成は良い仕組みですが、それに乗ることで「CMSに繋がらないと作業できない」という性質も一緒に受け取ります。その性質が邪魔になる場所だけ、外れてよいと判断しました。一貫性のために全部を揃えると、揃えた不便さも全部に広がります。

    robots.txt — 守っていないもの

    app/robots.ts を置くと /robots.txt が生成されます。中身はこれだけです。

    生成された robots.txt
    User-Agent: *
    Allow: /
    
    Host: https://www.kisaku.site
    Sitemap: https://www.kisaku.site/sitemap.xml

    ここで大事なのは、robots.txt はアクセス制限ではないことです。「クロールしてよい場所」を伝える取り決めであって、強制力はありません。守るかどうかは相手次第です。

    隠したいものを書いてはいけない

    「見られたくないから Disallow に書く」という使い方は逆効果です。robots.txt は誰でも読めるので、「ここに何かある」と書き残すことになります。本当に見せたくないものは、認証をかけるか、そもそも公開領域に置かないかのどちらかです。

    今回の実質的な役割は最後の1行、sitemapの場所を知らせることです。これがあると、Search Consoleに登録していない検索エンジンもsitemapを見つけられます。

    Host を書いているのは、このサイトが kisaku.site を www 側へ寄せているためです。どちらが正なのかを明示しています。

    構造化データ — 見た目から意味を切り離す

    3つめです。記事ページに、こういうものを埋め込みました。

    出力されるJSON-LD(抜粋)
    {
      "@type": "BlogPosting",
      "headline": "wwwのあるなしで、別のサイトになる",
      "datePublished": "2026-08-22T13:20:00",
      "dateModified":  "2026-08-22T14:21:46",
      "author":    { "@type": "Person", "name": "KiSAKU" },
      "publisher": { "@type": "Person", "name": "KiSAKU" }
    }

    画面には何も出ません。「これは記事で、公開日はこれで、書いたのはこの人」と直接伝えているだけです。

    これが無い場合、検索エンジンはHTMLの構造から推測します。「大きな文字だから見出しだろう」「日付っぽい文字列があるから公開日だろう」と。たいてい当たりますが、デザインを変えると解釈が変わることがあります。

    見た目を変えても、意味は動かない

    明示しておけば、日付の表示位置を変えても、見出しのサイズを変えても、伝わる内容は同じままです。装飾の変更が、意図せず情報を壊さない状態になります。

    パンくずも一緒に出す

    BreadcrumbList も併記しています。検索結果に出るパンくず表示のためです。

    階層
    ホーム › ブログ › (記事タイトル)

    ここで書く階層は、画面上の構造と一致させる必要があります。実際には無い階層を書くと、検索結果と実際のサイトが食い違い、逆効果になります。

    タグが途中で閉じないようにする

    細かいですが、実装上の注意点です。JSON-LDは <script> タグの中に書きます。もし記事タイトルに </script> という文字列が入っていたら、そこでタグが閉じてしまいます。

    対処
    JSON.stringify(jsonLd).replace(/</g, "\\u003c")

    < をユニコードのエスケープ表記に置き換えています。JSONとしての意味は変わらず、HTMLとしては無害になります。自分で書いた記事しか入らない場所でも、こういう箇所は塞いでおくほうが安いです。

    入れたら、実物で確かめる

    3つとも、公開後に実際のURLを叩いて確認しました。

    確認
    https://www.kisaku.site/robots.txt   → 4行が返る
    https://www.kisaku.site/sitemap.xml  → 固定ページ5 + 記事20 = 25件
    記事ページ → script[type="application/ld+json"] を読んで中身を検証

    特にsitemapは、件数を数えるまで安心できません。今回の20本の壁のように、正しく生成されているのに中身が足りない、という失敗があり得るからです。「エラーが出ていない」は「正しい」ではありません。

    今回の学び

    まず、3つとも「順位を上げる」ものではないこと。sitemapは発見までの時間を短くし、robots.txtは地図の場所を教え、構造化データは意味の推測を減らします。効果を正しく理解していないと、成果が出ないときに何を疑えばいいか分からなくなります。

    次に、似た名前のものを、同じものだと思わないこと。「記事一覧」という同じ名前でも、表示のための一覧と、検索エンジンへ渡す一覧は要件が違いました。流用は速いですが、要件が違うものを流用すると、後から静かに壊れます。

    そして、仕組みの利点は、全部に適用しなくてよいこと。型生成は良い仕組みですが、乗ると「CMSに繋がらないと変更できない」という性質も付いてきます。その性質が邪魔になる1か所だけ、外れる判断をしました。

    最後に、エラーが出ないことは、正しいことではないこと。sitemapの20本上限は、何のエラーも出さずに記事を落とし続けるところでした。出力されたものを数える、という確認をしなければ気づけませんでした。

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

    所有権をどう証明するか

    「このサイトはあなたのものですね」を、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か所すべてで同じホスト名に揃える必要がありました。

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

  • 「来なかった人」を見るために

    「来なかった人」を見るために

    アクセス解析は「来た人」しか教えてくれません。

    検索結果に出たのに押されなかった、そもそも検索結果に出ていない——こうした「来なかった人」の情報は、GA4には一切残りません。それを見るための道具がGoogle Search Consoleです。今回はその導入と、所有権の確認方法を選ぶまでの判断を記録します。

    この記事で扱うこと

    Search ConsoleとGA4の役割の違い、ドメインプロパティとURLプレフィックスの選び分け、5つある所有権確認方法のうちどれを選んだか。そして、確認コードをコードに直接書かなかった理由。

    GA4では見えないもの

    まず、2つの道具が何を見ているのかを整理します。境界を勘違いすると、片方に無いものを延々と探すことになります。

    Googleアナリティクス Search Console
    見る対象 サイトに来た後の行動 サイトに来る前の状況
    データの出どころ サイトに埋めたタグ Googleの検索システム
    分かること 閲覧ページ、滞在、遷移、流入元 検索語、表示回数、順位、クリック率
    分からないこと どんな言葉で探されたか サイト内で何をしたか

    この違いがはっきり出るのが検索語です。GA4は「Google検索から来た」ことは分かりますが、何と入力して来たのかは教えてくれません。プライバシー保護のため、検索エンジン側が渡していないからです。

    Search Consoleは検索エンジン側の記録なので、そこが見えます。さらにクリックされなかった表示も数えられています。これがGA4には原理的に存在しない情報です。

    表示されているのに押されていない、が分かる

    ある記事が検索結果に100回出て、クリックが2回だとします。GA4に残るのは「2」だけです。Search Consoleを見ると「100回出ている」と分かるので、内容ではなくタイトルの問題だと切り分けられます。逆に表示が0なら、書き方以前に見つけられていません。直す場所がまったく変わります。

    プロパティの種類を選ぶ

    登録の最初に、2種類のどちらかを選ばされます。

    ドメイン URL プレフィックス
    対象 すべてのサブドメイン、http/https両方 入力したアドレスの配下だけ
    確認方法 DNSの設定のみ 5種類から選べる
    向いている場面 サブドメインをまとめて見たい 特定の1サイトだけ見たい

    今回はURLプレフィックスを選び、https://www.kisaku.site/ を登録しました。

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

    もうひとつは、確認方法の選択肢です。ドメインプロパティはDNSレコードの追加が必須で、ドメイン管理会社の画面での作業になります。設定を間違えるとサイト全体が見えなくなる場所なので、必要のない作業は入れたくありませんでした。

    wwwの有無は別物として扱われる

    URLプレフィックスでは、https://kisaku.site/ と https://www.kisaku.site/ は別のプロパティです。転送を設定していても自動では統合されません。登録するのは転送先——つまり実際に表示されるほうです。ここを間違えると、いつまでもデータが0のままになります。

    所有権の確認方法

    URLを入力すると、そのサイトが自分のものだと証明するよう求められます。方法は5つ提示されました。

    方法 やること 今回の判断
    HTMLファイル 指定のファイルをサイト直下に置く ×
    HTMLタグ metaタグを head に入れる 採用
    Googleアナリティクス GA4のタグが入っていれば通る ×
    Googleタグマネージャー GTMのタグが入っていれば通る ×
    DNSレコード ドメイン設定にTXTを追加 ×

    HTMLファイル方式を外したのは、Next.jsで public に置くこと自体はできるものの、この1ファイルが何のためにあるのか、後から見て分からなくなるからです。ファイル名は意味を持たない英数字の羅列で、消しても当面は気づきません。そして消すと所有権が外れます。

    GA4やGTMを使う方式も外しました。一見いちばん楽ですが、計測を止めた瞬間に所有権の確認も外れます。将来「GTMをやめて直接タグを貼ろう」と判断したとき、Search Consoleまで巻き込まれます。関係のないもの同士を依存させない、という判断です。

    DNSは前述のとおり、影響範囲が大きすぎるので今回は使いませんでした。

    残ったHTMLタグ方式を選んでいます。head に1行入るだけで、これはNext.jsが正式に対応している範囲です。

    確認コードを直接書かなかった理由

    提示されるのはこういう1行です。

    Search Consoleが提示するタグ
    <meta name="google-site-verification" content="(英数字の文字列)" />

    これを layout.tsx にそのまま書いても動きます。しかし、環境変数から読む形にしました。

    layout.tsx(要点)
    export const metadata: Metadata = {
      metadataBase: new URL("https://www.kisaku.site"),
      // ...
      verification: {
        google: process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION,
      },
    };

    Next.jsには verification という専用の項目が用意されていて、値を渡すと head にmetaタグを出力してくれます。手書きでタグを差し込む必要はありません。

    環境変数にした理由は、値が未設定なら何も出力されないからです。コードを先にデプロイしておいて、確認コードを取得してから設定を足せます。実際、今回はコードのほうが先にできていました。

    もうひとつ、この値がサイトの機能に関係ないものだという点も理由です。動作には一切影響しないので、コードの中に混ざっていると「何のための文字列か」を毎回思い出すことになります。設定として外に置けば、設定であることが見た目で分かります。

    この値は秘密ではない

    NEXT_PUBLIC_ が付いた環境変数は、ビルド時にコードへ埋め込まれ、誰でも見られる状態になります。この確認コードはもともとHTMLに出力するためのものなので、それで問題ありません。逆に言えば、秘密にすべき値をこの接頭辞で扱ってはいけません。接頭辞が「公開してよい」という宣言になっています。

    登録しただけでは確認されない

    metaタグがまだ本番に出ていないので、この時点でのプロパティは未確認の状態です。

    手順としてはこうなります。

    確認が通るまでの順番
    1. Search Consoleでプロパティを追加し、確認コードを控える
    2. Vercelの環境変数に確認コードを登録する
    3. 再デプロイする(環境変数の追加だけでは反映されない)
    4. Search Consoleに戻って「確認」を押す
    環境変数は追加しただけでは効かない

    NEXT_PUBLIC_ の値はビルド時にコードへ埋め込まれます。管理画面で追加しても、その後にビルドし直さなければ、出力されるHTMLは前のままです。「設定したのに反映されない」の大半はこれです。

    確認が通ったあとは、サイトマップの送信が次の作業になります。sitemap.xml をまだ用意していないので、これは別のタスクとして残しました。

    今回の学び

    まず、2つの道具は「来た後」と「来る前」で分かれているということ。GA4だけを見ていると、記事が読まれない理由を内容の問題だと考えがちです。実際には検索結果に出ていないだけかもしれません。直す場所を間違えないために、両方が要ります。

    次に、楽な確認方法ほど、他のものに依存していること。GA4やGTMを使う方式は一瞬で終わりますが、計測をやめた瞬間に所有権も外れます。関係のないもの同士を結びつけない——遠回りに見えても、独立させておくほうが後で自由が利きます。

    そして、設定はコードの外に置くと、設定に見えること。確認コードを直接書いても動きますが、コードの中に混ざると意味を思い出す手間が毎回かかります。環境変数にしておけば、それが「あとで変わりうる値」だと見た目で分かります。