GSCの「CTR 0%」は目次リンクだった|自動リライト誤判定

週末起業ラボのアイキャッチ画像:Search Console APIの#付き行でCTRを誤判定した原因と修正を解説するブログ記事 ブログ運営

2026年10月10日の朝、Slackに同じ形の通知が19件並んだ。「2回下書きを作成しても改善なし。手動対応が必要です」。Search ConsoleのAPIでクリック率の低い記事を拾い、タイトルとメタディスクリプションを書き直す処理を毎週回している。その処理が、19記事を「もう自動では直せない」と投げてきた。

調べると、19件のうち15件は直す必要のない記事だった。台帳には「CTR 0%」と記録されていたが、記事本体のクリック率は順位相応か、それ以上に出ていた。処理が見ていたのは記事本体の行ではなく、URLの末尾に #toc1 のような目次アンカーが付いた別の行だった。

TL;DR / 三行要約 6分 MIN READ · UPDATED 2026-10-12
  1. GSC APIをページ単位で引くと、`#toc1` などのアンカー付きURLが別の行で返ってくる。3サイト・90日で計417行、表示15万1,277回に対してクリックは3回だった。
  2. CTRで改善候補を選ぶ処理は、クリック率が正常な本体行を先に外すので、CTRがほぼ0のアンカー行だけが残って候補になる。
  3. 直し方は数行。判定の前に `#` を含む行を捨てる。
RESULT — 誤判定15件を特定 TOOL — Search Console API COST — 0円

何が起きたか|エスカレーション19件のうち15件は正常だった

自動リライトの流れ

処理は毎週土曜の朝に自動で動く。スケジューラから無人で回す部分は、無人実行をAPI課金なしで回す手順のとおりに組んでいる。流れは次のとおり。

改善候補の選定から判定まで
  • STEP1
    候補を拾う

    Search Console APIから直近28日のページ別データを取り、表示50回以上で、クリック率が順位ごとの期待値の0.7倍に届かないページを台帳に入れる(15位より下のページはクリック率に関係なく入る)。

  • STEP2
    下書きを作る

    LLMにタイトルとメタディスクリプションの案を作らせ、WordPressに書き換え用の下書きを作る。公開するかどうかは人が決める。

  • STEP3
    30日後に判定

    公開から30日後にもう一度データを取る。改善候補の一覧から消えていれば改善。残っていれば試行回数を1増やし、2回目でも改善しなければSlackへエスカレーションする。

順位ごとの期待値は、コードに持たせた近似表を線形補間して出している。1位28%、3位11%、5位6%、10位2.5%といった値で、出典を特定した数字ではない。目安として使っている。

主力記事が「CTR 0%」になっていた

いちばん分かりやすかったのが、筆者が運営する別サイト(以下サイトC)でPVが多い、ある製品のレビュー記事だ。台帳には「3.9位・表示1,589回・CTR 0%」と記録されていた。順位3.9位でクリックが1件もないなら、たしかに大問題だ。

ところが、同じ期間(2026年6月20日〜7月19日)のデータをページ単位で引き直すと、こうなった。

行のURL表示クリックCTR平均順位
記事URL(本体)2,5262459.7%5.4
記事URL#toc11,62700%3.9
記事URL#toc41,54000%4.0
記事URL#toc71,53900%3.9
記事URL#toc101,53900%3.9
記事URL#toc141,62800%3.9

本体のCTRは9.7%。5.4位の期待値(約5.6%)を上回っている。台帳に入っていたのは、このうち #toc14 の行だった。

残りの14件も同じだった。19件中15件が #toc 付きの行で台帳に入っていて、記事本体のCTRは期待値の92%〜330%。改善の必要はない。

ムラサキ
ムラサキ

最初は「順位しか見ない判定が悪い」と思って、判定ロジックを直す計画まで書いた。台帳のエントリを1件ずつ開いて、raw_url の末尾に #toc14 が付いているのに気づいたのはその後だった。

原因|#付きの行にはクリックがほぼ付かない

3サイト・90日の集計

1記事だけの偶然かもしれないので、筆者が運営する3サイト全体で確かめた。このブログ以外の2サイトはジャンルが違うので、サイトB・サイトCと書く。期間は2026年7月12日〜10月9日の90日、ページ単位で取れる行をすべて取得している。

サイト#付きの行#行の表示#行のクリック本体の行本体の表示本体のクリック
週末起業ラボ9717,19805632,7101,267
サイトB6910,96904919,576755
サイトC251123,11036986,4085,772

# 付きの行は3サイトで計417行。本体の行(174行)より多い。表示は合わせて15万1,277回あるのに、クリックはサイトCの3回だけだった。

サイトCでは、# 付き行の表示(123,110回)が本体の表示(86,408回)を上回っている。ページ別の行をそのまま合計すると、表示回数が大きく水増しされる。

アンカー付きの行が何なのかは、公式には書かれていない

APIリファレンスのsearchanalytics.queryにも、Search Consoleヘルプの検索パフォーマンスレポートの説明とデータの不一致の説明にも、URLの # 以降の扱いは書かれていない(2026年10月12日に確認)。後者には「データは原則としてGoogleが選んだ正規URLに割り当てられるが、実際のURLに割り当てられることもある」とあるだけだ。

ここからは推測になる。このレビュー記事では、#toc の行はどれも3.9位前後、表示1,540〜1,630回でほぼ揃っていた。検索結果で記事の下に並ぶ目次へのジャンプリンクが、まとめて表示されたぶんではないかと見ている。確かめる手段は今のところない。

なぜ本体ではなく#行が残ったのか

処理の順番がそうさせていた。

graph TD
  A["ページ別の行を取得"] --> B{"表示50回以上?"}
  B -->|"いいえ"| X["除外"]
  B -->|"はい"| C{"CTRが期待値の0.7倍以上?"}
  C -->|"はい"| Y["正常として除外"]
  C -->|"いいえ"| D["改善候補として台帳へ"]

本体の行はCTRが正常なので、2つ目の判定で外れる。#toc の行はCTRがほぼ0なので、必ず候補に残る。URLから # 以降を削って同じ記事として扱う正規化は入れてあったが、それは候補に残った行どうしの重複をまとめる処理だった。本体の行は、その時点でもう除外されている。

30日後の判定も抜け出せない。判定は「改善候補の一覧から消えたら改善」だが、同じ記事の #toc 行は一覧に残り続ける。2回目の下書きを作るときも #toc 付きのURLで数字を取り直すので、CTRは何度書き直しても0のまま。結果は「2回直しても改善なし」になる。

直し方|判定の前に#付きの行を捨てる

筆者が動作確認済み(環境: Windows 11・Python 3.13・google-api-python-client)。

修正は、行を読み込んだ直後に # 付きを捨てるだけだった。

for row in resp.get("rows", []):
    raw_url = row["keys"][0]
    if "#" in raw_url:
        continue          # 目次アンカーの行はCTRが常にほぼ0。判定に使わない
    url = raw_url.split("?")[0]
    # ここから先の「表示50回以上」「期待CTRの0.7倍」の判定は本体行だけで行う

台帳に残っている古いエントリのために、下書きを作るときに # 付きのURLが渡されたら記事本体のURLで引き直す処理も1行足した。

テストは2つ書いた。本体のCTRが正常で #toc の行だけCTR 0のときは、候補が0件になること。本体のCTRが本当に低いときは、#toc の行が混ざっていても本体の数字で1件返ること。修正前のコードに戻すと、2つとも失敗することまで確かめた。

修正後に実データで回すと、# 付きの行は3サイトとも0件になった。候補は週末起業ラボ12件、サイトB9件、サイトC5件。

誤データで変えたタイトルは戻すべきか

戻す必要はなかった

台帳には #toc 起点のエントリが33件あった。このうち11記事は、2026年9月中旬以降に誤った診断をもとにタイトルが変わっていた(9月13日より前のタイトル変更は、別の不具合で検索結果に出ない欄に書いていたので、実質的に効いていない)。

下書きを作る前の28日と、直近28日のCTRを比べた。どちらの期間でも、期待値の70%を下回った記事はなかった。直近28日でいちばん低かったのはサイトBの記事で110%。週末起業ラボの2記事は、5時間制限の記事が期待値の201%→204%、AI画像生成の記事が104%→149%だった。

ただし、変更から日が浅い記事は直近28日に変更前の日も含んでいる。1記事は10月10日に変わったばかりで、変更後のデータはまだない。

戻せば、またタイトルが変わる。悪化していないものをわざわざ戻す理由はないので、そのままにした。7ヶ月目のレポートで「この期間に記事の手直しを大量にやっている」と書いたが、その一部はこの誤判定から始まっていたことになる。

本当にCTRが低かった4件

19件のうち4件は、# の付かない本体の行で拾われていた。こちらは順位7.1〜8.6位でCTR 0〜0.9%。本当に低い。

ただし、この4件も2回の試行は9月13日より前で、タイトルの変更は一度も検索結果に出ていなかった。なので「直しても効かなかった」とは言えない。3件はタイトルかメタディスクリプションを書き直して公開した(1件はメタだけ)。残る1件は検索クエリが1つしか取れず、案を作る材料がないので見送った。

同じ処理を書くなら気をつけること

ページ別の行を足し算しない

上の表のとおり、サイトCでは # 付きの行の表示が本体を上回る。ページ別の行を合計してサイト全体の表示回数にすると、実際より大きく出る。サイト全体の数字が欲しいなら、ページで分けずに取る。ヘルプにも、ページで分けた表はページ単位、それ以外はプロパティ単位で集計するとあり、数字が合わない主な原因はこの集計単位の違いだと書かれている。

URLで絞り込むときは本体のURLで

APIの equals フィルタはURLの完全一致だ。#toc14 付きのURLで絞れば、#toc14 の行の数字しか返らない。今回、判定のたびにCTR 0が返り続けたのはこのためだった。

「0」が出たら生データを1件見る

CTR 0%で3.9位、表示1,500回。この数字は、主力記事として不自然だった。最初にそこで生データを引いていれば、判定ロジックを直す計画を書く前に気づけた。

よくある質問(FAQ)

Q
#付きの行を捨てると、ジャンプリンク経由のクリックを取りこぼしませんか?
A

3サイト・90日で、#付きの行についたクリックは計3回だった(サイトCのみ)。表示15万1,277回に対して3回なので、改善候補を選ぶ目的なら無視できる量だと判断した。本体の行が、ジャンプリンク経由のクリックを含んでいるかどうかは確認できていない。

Q
自動で判定させず、Search Consoleの画面を見て選ぶほうが確実では?
A

記事が増えると毎週は見きれないので、候補を拾うところまでは自動にしている。ただ今回のように、拾い方がずれていると正常な記事が候補に並び続ける。エスカレーションが急に増えたら、台帳の数字を信じる前に1記事だけAPIの生データと突き合わせる。15件の誤判定はそれで見つかった。

CTRで記事を選ぶなら、#付きの行を先に捨てる

GSC APIのページ別データには、記事本体とは別に #toc の行が混ざる。その行はクリックがほぼ付かないので、CTRで「低い記事」を探す処理にかけると、正常な記事ほど拾われる。判定の前に # 付きの行を捨てる。それだけで今回の19件中15件は出なかった。

LLMに書かせたタイトルの数字を疑う話はAIが数字を捏造した事故と防ぐ4ルールに書いたが、今回は集計そのものが間違っていた。自動化した部分の出力は、主力記事の数字で一度だけでも手で確かめておく。

ムラサキ
ムラサキ
SE歴20年 · 金融系システム開発課長 · 週末起業1年目
システムエンジニア歴20年。金融系システム開発の課長として勤務する傍ら、AIと副業の最前線を実践レポート。Claude Code Max 5xプランを日常業務・副業開発に活用中。一次情報と実測値にこだわった記事を発信。
20 YRS SE
8 MOS 副業
55 ARTICLES
タイトルとURLをコピーしました