0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「1位なのにCTRが落ちた」を疑うな、実測しろ — Search Console APIで“面積の地盤沈下”を可視化する

0
Posted at

📝 この記事は自社ブログの戦略レポート(CodeQuest.work)を、エンジニア向けに「計測実装」へ振り直して再構成したものです。戦略の背景に興味がある方は原文をどうぞ。

結論(先に)

「順位は維持されているのに CTR が落ちている」クエリは、Search Console API の query × date 集計で機械的に検出できます。 これは広告主優遇の陰謀ではなく、SERP(検索結果ページ)という限られた面積を、広告枠・AI Overviews・リッチリザルトが奪う 「面積の地盤沈下」 が原因です。順位レポートだけ見ていると見落とすので、本記事では Python で「同順位 CTR 低下クエリ」を抽出するコードを実装します。

対象読者は、GA4 や Search Console を見て「最近オーガニックが弱い気がする」と感じているが、それを数字で言語化できていない個人開発者・Web 制作者です。

なぜ順位レポートでは見えないのか

順位(平均掲載順位)は「あなたのページが検索結果の何番目に出たか」しか表しません。しかし実際のユーザーが見る画面では、同じ「1位」でも前後の状況が年々変わっています。

圧力要因 SERP 面積への影響
広告枠の拡大 最上部の広告数・面積が増え、純粋なオーガニックが下へ押し下げられる
AI Overviews(AIO) 冒頭にAI回答が大きく表示され、リンク到達前に答えが得られる
リッチリザルト・自社プロパティ 強調スニペット・地図・ショッピング・動画枠が面積を占有する

結果、こういう現象が起きます。

  • 「10位以内なのに、ファーストビューに映らない」
  • 「1位なのに、CTR が以前より低い」

これが 面積の地盤沈下 です。順位(rank)は同じでも、その順位が置かれている“地盤”そのものが沈む。だから 順位ではなく「順位 × CTR の時系列」で見ないと検出できません。

興味深いことに、順位と AI 引用は単純な代替関係ではありません。

  • Seer Interactive の分析:AIO 引用の約 55% がページ上部30%から抽出される
  • Ahrefs の調査:AI 引用の 62% がオーガニックトップ10圏外から来ている

「Google順位が低くても AI には引用される」逆転も起きており、単一指標の「順位」はもう現実を説明しきれません。だからこそ自分のデータで実測します。

実装:Search Console API で「同順位 CTR 低下クエリ」を抽出する

1. 準備

Google Cloud でサービスアカウントを作り、Search Console の対象プロパティに「制限付き」ユーザーとして当該サービスアカウントのメールを追加します。あとはライブラリを入れるだけです。

pip install google-api-python-client google-auth

2. 期間を2分割して取得するクライアント

「先月 vs 今月」のように2期間を比較し、順位がほぼ同じなのに CTR が落ちたクエリを炙り出すのが狙いです。Search Console API は dimensionsquery を指定すると、クエリごとの clicks / impressions / ctr / position を返します。

from googleapiclient.discovery import build
from google.oauth2 import service_account

SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]
SITE_URL = "sc-domain:example.com"  # ドメインプロパティの場合
KEY_FILE = "service-account.json"


def build_client():
    """Search Console API クライアントを生成する"""
    credentials = service_account.Credentials.from_service_account_file(
        KEY_FILE, scopes=SCOPES
    )
    return build("searchconsole", "v1", credentials=credentials)


def fetch_query_stats(service, start_date: str, end_date: str) -> dict:
    """指定期間のクエリ別 clicks/impressions/ctr/position を取得する

    戻り値: { query: {"clicks", "impressions", "ctr", "position"} }
    """
    rows_by_query: dict[str, dict] = {}
    start_row = 0

    while True:
        request = {
            "startDate": start_date,
            "endDate": end_date,
            "dimensions": ["query"],
            "rowLimit": 25000,
            "startRow": start_row,
        }
        response = (
            service.searchanalytics()
            .query(siteUrl=SITE_URL, body=request)
            .execute()
        )
        rows = response.get("rows", [])
        if not rows:
            break

        for row in rows:
            query = row["keys"][0]
            rows_by_query[query] = {
                "clicks": row["clicks"],
                "impressions": row["impressions"],
                "ctr": row["ctr"],
                "position": row["position"],
            }

        if len(rows) < 25000:
            break
        start_row += 25000

    return rows_by_query

ポイントは rowLimit: 25000 でページングしている点です。クエリ数は数万件になることが普通なので、startRow を進めて全件取得します。

3. 「順位据え置き × CTR 低下」を判定する

2期間のクエリ統計を突き合わせ、順位の変動が小さい(地盤沈下の疑い)のに CTR が有意に落ちたクエリを抽出します。

def detect_ctr_erosion(
    prev: dict,
    curr: dict,
    min_impressions: int = 100,
    position_tolerance: float = 1.0,
    ctr_drop_threshold: float = 0.15,
) -> list[dict]:
    """順位はほぼ維持なのに CTR が落ちたクエリを抽出する

    - min_impressions: ノイズ除去のための最小表示回数
    - position_tolerance: 「順位据え置き」とみなす平均順位の差(±)
    - ctr_drop_threshold: CTR 低下率の閾値(0.15 = 15%以上の低下)
    """
    eroded = []

    for query, now in curr.items():
        before = prev.get(query)
        if before is None:
            continue
        if now["impressions"] < min_impressions:
            continue

        position_shift = now["position"] - before["position"]
        # 順位が悪化(数値増)しすぎ/改善しすぎは対象外(純粋な順位変動のため)
        if abs(position_shift) > position_tolerance:
            continue
        if before["ctr"] == 0:
            continue

        ctr_drop_rate = (before["ctr"] - now["ctr"]) / before["ctr"]
        if ctr_drop_rate < ctr_drop_threshold:
            continue

        eroded.append({
            "query": query,
            "position_before": round(before["position"], 1),
            "position_after": round(now["position"], 1),
            "ctr_before": round(before["ctr"] * 100, 2),
            "ctr_after": round(now["ctr"] * 100, 2),
            "ctr_drop_rate": round(ctr_drop_rate * 100, 1),
            "impressions": now["impressions"],
        })

    # 影響の大きい順(低下率 × 表示回数)に並べる
    eroded.sort(key=lambda r: r["ctr_drop_rate"] * r["impressions"], reverse=True)
    return eroded

4. 実行

def main():
    service = build_client()
    prev = fetch_query_stats(service, "2026-03-01", "2026-03-31")
    curr = fetch_query_stats(service, "2026-04-01", "2026-04-30")

    eroded = detect_ctr_erosion(prev, curr)

    print(f"{'query':<30} pos  CTR(before→after)  drop%   impr")
    for r in eroded[:20]:
        print(
            f"{r['query']:<30} "
            f"{r['position_before']}{r['position_after']}  "
            f"{r['ctr_before']}%→{r['ctr_after']}%  "
            f"-{r['ctr_drop_rate']}%  {r['impressions']}"
        )


if __name__ == "__main__":
    main()

出力イメージ(数値はサンプル):

query                          pos  CTR(before→after)  drop%   impr
nextjs ssg 実装                 2.1→2.3  8.4%→5.1%  -39.3%  1820
seo チェック ツール             1.4→1.5  12.0%→8.8%  -26.7%  3400
faq 構造化データ                3.2→3.1  6.1%→4.9%  -19.7%  980

順位はほぼ動いていないのに CTR だけが二桁%落ちているクエリ が並んでいたら、それが地盤沈下の候補です。これらのクエリの実際の SERP を手で確認すると、AI Overviews が出ていたり、広告枠やショッピング枠が増えていたりするはずです。

「読まれる状態」になっているか:AIクローラー向けSSRチェック

地盤沈下への対抗策の一つが「AI に引用される側になる」ことですが、その前提として AIクローラーがあなたのページを読めるか を確認する必要があります。AIクローラー(GPTBot, ClaudeBot, PerplexityBot など)は JavaScript レンダリングを公式には保証していません。 SPA 構成だと「Google には映るが AI には映らない」状態になりがちです。

「JS なしで本文 HTML が出力されているか」は、curl で UA を偽装して簡易チェックできます。

# AIクローラーの代表的な UA で取得し、本文がHTMLに含まれるか確認
curl -s -A "Mozilla/5.0 (compatible; GPTBot/1.0; +https://openai.com/gptbot)" \
  https://example.com/your-article | grep -c "記事内の特徴的な一文"

0 が返ってきたら、その本文はクライアントサイドでしか描画されていない=AIクローラーには空ページに見えている可能性が高いです。Python でレンダリング前後を比較するとより確実です。

import requests

AI_BOT_UA = "Mozilla/5.0 (compatible; GPTBot/1.0; +https://openai.com/gptbot)"
MARKER = "記事内の特徴的な一文"  # 本文中の一意なフレーズ


def is_readable_by_ai_crawler(url: str) -> bool:
    """JSなしの素のHTMLに本文が含まれているかを判定する"""
    res = requests.get(url, headers={"User-Agent": AI_BOT_UA}, timeout=10)
    res.raise_for_status()
    return MARKER in res.text


print("AI可読:" , is_readable_by_ai_crawler("https://example.com/your-article"))

対策は明快で、SSR/SSG で本文 HTML を出力すること。Next.js なら App Router の Server Component や generateStaticParams、もしくは ISR で、本文がレスポンス HTML に含まれる状態を作ります。引用される以前に、まず読まれる状態が先です。

引用されやすくする:直接回答 × 構造化データ

AI に引用される情報源の条件はシンプルで、①論点への明快な直接回答がある ②根拠と出典を持つ ③構造化されている の3点です。小手先の裏技はなく、結局「良いSEOそのもの」に収束します。

実装で効くのは、FAQ を構造化データ(JSON-LD)で明示することです。質問と回答のペアは AI が抽出しやすく、強調スニペットにも乗りやすい形式です。

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "同順位なのにCTRが落ちる原因は?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "SERPの面積を広告枠・AI Overviews・リッチリザルトが奪う『面積の地盤沈下』が主因です。順位ではなく順位×CTRの時系列で検出します。"
      }
    },
    {
      "@type": "Question",
      "name": "順位が低いページはAIに引用されない?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "いいえ。Ahrefsの調査ではAI引用の62%がトップ10圏外から来ており、順位とAI引用は単純な代替関係ではありません。"
      }
    }
  ]
}
</script>

ポイントは、acceptedAnswer の冒頭1〜2文で結論を言い切ることです。AIO引用の約55%がページ上部から抽出されるという傾向(Seer Interactive)に合わせ、回答文の先頭に結論、後ろに補足、の順で書きます。本文の各セクションも同じく「結論ファースト」にすると引用率が上がります。

まとめ:評価軸を「順位」から「面積 × クリック後の信頼」へ

陰謀論を恨んでも前進しません。「面積を取られる」前提で計測を組み替えるのが現実的な打ち手です。

  1. 検出:Search Console API で「順位据え置き × CTR 低下クエリ」を定期抽出する(本記事のコード)
  2. 可読性:AIクローラー UA で SSR/SSG が効いているか確認する
  3. 引用最適化:直接回答 + FAQ 構造化データで「引用されやすい形」にする
  4. 継続計測:順位が下がってからではなく、CTR の兆候で先手を打つ

順位が下がるのは結果であって、その前に CTR という先行指標が動きます。そこを機械的に拾えるようにしておくのが、地盤沈下の時代の守り方です。

戦略的な背景(なぜGoogleが今さら自社広告を打つのか、オーガニック=信頼の堀という逆説)に踏み込んだ原文はこちらです。

🔗 AI検索の脅威と『オーガニック=信頼の堀』という逆説(CodeQuest.work)


SEOスコアチェックツール: SEO_CHECK — RINIAディレクターツール。45項目チェックで、自分のページが「面積とCTR」の観点で健全かを無料診断できます。
Web制作・SEO関連の技術情報サイト: CodeQuest.work

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?