2026年6月20日

2026年6月20日

WordPressの記事のインデックス状況をGoogleサーチコンソールAPIで自動監視する

WordPressの記事がGoogleにインデックスされているかを自動で見張るには、Search Console API の「URL 検査 API」を使い、記事の URL を定期的に検査して前回の結果との違いを通知する仕組みを作ります。この記事では、サービスアカウントの準備から、WordPress の REST API で記事一覧を取得して検査する Python スクリプト、サーバーの cron での定期実行、エラー時の切り分けまでを順に解説します。

最初に、API でできることとできないことを整理しておきます。ここを誤解したまま作り始めると、期待した監視にならないためです。

Search Console API でできること・できないこと

  • できる:URL 検査 API で、1つの URL ごとに Google のインデックス登録の状態(判定、登録状況の説明、最終クロール日時、Google が選んだ正規 URL など)を取得する
  • できる:検索アナリティクス API で、ページごとの表示回数やクリック数を取得する。表示回数が急に途絶えたページを見つける補助になる
  • できない:Search Console の画面にある「ページのインデックス登録」レポートの一覧をそのまま取得する API はない
  • できない:URL 検査の「公開 URL をテスト」(ライブテスト)や「インデックス登録をリクエスト」は API から実行できない

また、Google の Indexing API は、求人情報やライブ配信のページなど対象が限られた API で、一般的なブログ記事のインデックス登録を早める用途には使えません。この記事で使うのは、状態を読み取るだけの URL 検査 API です。

URL 検査 API には、プロパティごとに1日あたり・1分あたりの利用上限があります。上限の値は変わることがあるため、Google の公式ドキュメントで確認してください(2026年10月現在)。記事数が多いサイトでは、1回の実行で全記事を検査するのではなく、新しい記事や更新した記事を優先して、少しずつ巡回する設計にします。

全体の構成と、WordPress の外で動かす理由

構成は次のとおりです。

  1. WordPress の REST API から公開済み記事の URL と更新日時を取得する
  2. 未検査の記事、前回の検査後に更新された記事、検査から日数がたった記事の順に選ぶ
  3. URL 検査 API で状態を取得し、前回の結果と比べる
  4. 新しく問題が見つかった URL だけを通知する

このスクリプトは、WordPress のプラグインとしてではなく、同じサーバー(または別のサーバー)で cron から動かす独立したスクリプトにするのがおすすめです。WP-Cron はサイトへのアクセスをきっかけに動くため実行時刻が安定せず、API を何百回も呼ぶ長い処理を Web のリクエストの中で動かすと、タイムアウトやサイトの遅延の原因になります。認証用の鍵ファイルを WordPress のディレクトリに置かずに済む点も利点です。

WordPress の中で完結させたい場合は、Google が公開している PHP 用クライアントライブラリ(Composer の google/apiclient)をプラグインに組み込み、サーバーの cron から WP-CLI のコマンドとして実行する方法があります。ライブラリのクラス名や使い方はバージョンで変わることがあるため、その場合はライブラリの公式ドキュメントに従ってください。認証の署名処理を自作するのは避け、公式ライブラリを使います。

準備:サービスアカウントと権限

  1. Google Cloud のコンソールでプロジェクトを作り、「Google Search Console API」を有効にする
  2. サービスアカウントを作成し、JSON 形式の鍵を作成してダウンロードする
  3. Search Console の対象プロパティの「設定」にあるユーザーと権限の画面で、サービスアカウントのメールアドレスをユーザーとして追加する

鍵ファイルはパスワードと同じ扱いです。Web から見えないディレクトリ(ドキュメントルートの外)に置き、所有者だけが読めるパーミッション(例:600)にして、Git には含めません。追加する権限は、URL 検査が使える範囲でできるだけ小さいものから試してください。組織のポリシーでサービスアカウントの鍵の作成が禁止されている場合は、OAuth の利用者認証を使う方法を検討します。画面の名称は変わることがあるので、見つからない場合は目的(API の有効化、鍵の作成、ユーザーの追加)に合う項目を探してください。

プロパティの指定方法にも注意が必要です。ドメインプロパティなら sc-domain:example.com、URL プレフィックスのプロパティなら https://example.com/ のように、Search Console に登録した形と一致させます。

監視スクリプトの例(Python)

サーバーに Python の仮想環境を作り、必要なライブラリを入れます。

mkdir -p ~/gsc-monitor && cd ~/gsc-monitor
python3 -m venv venv
venv/bin/pip install google-api-python-client google-auth requests

スクリプト本体の例です。サイトの URL やファイルの場所は、自分の環境に合わせて環境変数で指定します。

#!/usr/bin/env python3
"""WordPress の記事 URL を URL 検査 API で調べ、新しく見つかった問題だけを出力する。"""
import json
import os
import sys
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path

import requests
from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

KEY_FILE = os.environ["GSC_KEY_FILE"]  # ドキュメントルートの外に置いた鍵ファイル
SITE_URL = os.environ.get("GSC_SITE_URL", "sc-domain:example.com")
WP_URL = os.environ.get("WP_URL", "https://example.com")
STATE_FILE = Path(os.environ.get("GSC_STATE_FILE", str(Path.home() / "gsc-monitor" / "state.json")))
MAX_PER_RUN = int(os.environ.get("GSC_MAX_PER_RUN", "100"))  # 1回に検査する件数
GRACE_DAYS = 14  # 公開直後の記事は未登録でも通知しない日数
SCOPES = ["https://www.googleapis.com/auth/webmasters.readonly"]


def wp_posts():
    """REST API から公開済み投稿の URL と日時を取得する。"""
    page, items = 1, []
    while True:
        r = requests.get(
            f"{WP_URL}/wp-json/wp/v2/posts",
            params={"per_page": 100, "page": page, "_fields": "link,date_gmt,modified_gmt"},
            timeout=30,
        )
        r.raise_for_status()
        items += r.json()
        if page >= int(r.headers.get("X-WP-TotalPages", 1)):
            return items
        page += 1


def pick(posts, state):
    """未検査 → 検査後に更新 → 検査が古い順に並べて、上限件数だけ選ぶ。"""
    def key(p):
        s = state.get(p["link"])
        if s is None:
            return (0, "")
        if p["modified_gmt"] > s["modified_gmt"]:
            return (1, s["checked"])
        return (2, s["checked"])
    return sorted(posts, key=key)[:MAX_PER_RUN]


def inspect(service, url):
    body = {"inspectionUrl": url, "siteUrl": SITE_URL, "languageCode": "ja"}
    res = service.urlInspection().index().inspect(body=body).execute()
    r = res["inspectionResult"]["indexStatusResult"]
    keys = ("verdict", "coverageState", "robotsTxtState", "pageFetchState",
            "googleCanonical", "userCanonical", "lastCrawlTime")
    return {k: r.get(k) for k in keys}


def problems(post, result):
    found = []
    published = datetime.fromisoformat(post["date_gmt"]).replace(tzinfo=timezone.utc)
    is_new = datetime.now(timezone.utc) - published < timedelta(days=GRACE_DAYS)
    if result["verdict"] != "PASS" and not is_new:
        found.append(f"verdict={result['verdict']} ({result['coverageState']})")
    g, u = result["googleCanonical"], result["userCanonical"]
    if g and u and g != u:
        found.append(f"canonical mismatch: google={g}")
    return found


def main():
    creds = service_account.Credentials.from_service_account_file(KEY_FILE, scopes=SCOPES)
    service = build("searchconsole", "v1", credentials=creds, cache_discovery=False)
    state = json.loads(STATE_FILE.read_text(encoding="utf-8")) if STATE_FILE.exists() else {}
    alerts = []

    for post in pick(wp_posts(), state):
        url = post["link"]
        try:
            result = inspect(service, url)
        except HttpError as e:
            if e.resp.status == 429:
                print("quota exceeded, stop this run", file=sys.stderr)
                break
            print(f"API error {e.resp.status}: {url}", file=sys.stderr)
            continue

        found = problems(post, result)
        before = state.get(url, {}).get("problems", [])
        alerts += [f"{url}: {m}" for m in found if m not in before]
        state[url] = {
            "result": result,
            "problems": found,
            "modified_gmt": post["modified_gmt"],
            "checked": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S"),
        }
        time.sleep(1)  # 1分あたりの上限に近づかないよう間隔を空ける

    STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
    tmp = STATE_FILE.with_suffix(".tmp")
    tmp.write_text(json.dumps(state, ensure_ascii=False, indent=1), encoding="utf-8")
    tmp.replace(STATE_FILE)

    if alerts:
        print("\n".join(alerts))


if __name__ == "__main__":
    main()

このスクリプトは、判定(verdict)が PASS 以外になった記事と、Google が選んだ正規 URL が記事側の指定と食い違う記事を「問題」として扱い、前回の実行では出ていなかった問題だけを出力します。毎回同じ URL を通知し続けると、通知が読まれなくなるからです。

判定には、文章で書かれた coverageState ではなく、PASS・NEUTRAL・FAIL などの決まった値を返す verdict を使っています。coverageState は表示言語によって文言が変わることがあるため、比較には向きません。公開直後の記事は、まだ Google に知られていない状態(多くは NEUTRAL)になるのが普通なので、GRACE_DAYS の間は判定による通知を止めています。

固定ページも対象にしたい場合は、/wp-json/wp/v2/pages も同じように取得します。セキュリティプラグインなどで REST API の一覧取得を制限している場合は、WordPress 5.5 以降に標準で出力される XML サイトマップ(/wp-sitemap.xml、SEO プラグインを使っているとそちらのサイトマップに置き換わることがあります)から URL を集める方法に切り替えます。

cron で定期実行する

crontab に、1日1回実行する設定を追加します。cron は、コマンドの出力があると MAILTO に指定した宛先へメールを送るため、問題があった日だけメールが届く仕組みになります。

MAILTO=admin@example.com
30 4 * * * GSC_KEY_FILE=$HOME/secrets/gsc-monitor.json GSC_SITE_URL=sc-domain:example.com WP_URL=https://example.com $HOME/gsc-monitor/venv/bin/python $HOME/gsc-monitor/gsc_monitor.py

サーバーからメールが送れない環境では、出力を Slack の Incoming Webhook などに送る処理を足します。その場合も Webhook の URL は秘密情報として扱い、スクリプトに直接書かず環境変数や権限を絞ったファイルから読み込みます。

うまく動かないときの確認

API のエラーは、ステータスコードとメッセージから原因を絞り込めます。起きやすい順に並べます。

1. 403 で権限が足りないと返る

最も多いのは、サービスアカウントが Search Console のプロパティにユーザーとして追加されていないケースです。次に、SITE_URL の形がプロパティと一致していないケース(sc-domain: の付け忘れ、URL プレフィックスの末尾のスラッシュ漏れなど)を疑います。API が有効になっていないという趣旨のメッセージなら、Google Cloud 側で Search Console API を有効にしたプロジェクトと、鍵を作ったプロジェクトが同じかを確認します。

2. 400 で引数が不正と返る

検査する URL が、指定したプロパティの範囲外です。http と https、www の有無の違いでも範囲外になります。WordPress の「設定」→「一般」のサイトアドレスと、Search Console のプロパティがそろっているかを確認します。

3. 429 が返る

利用上限に達しています。スクリプトはその回の処理を打ち切り、次回の実行で続きから検査します。頻繁に出る場合は MAX_PER_RUN を減らします。

4. 結果が画面と違う

URL 検査 API が返すのは、Google のインデックスに登録されている情報で、最新のページを今その場で取得した結果ではありません。記事を直した直後は古い情報が返るのが普通です。

通知を受けたあとの対応

  • noindex が付いている:テーマや SEO プラグインの設定、「設定」→「表示設定」の検索エンジンでの表示の項目を確認する
  • robots.txt でブロックされている:robotsTxtState を確認し、robots.txt の記述を見直す
  • 正規 URL が食い違う:重複した内容のページがないか、canonical の指定が意図どおりかを確認する
  • クロールはされたが登録されない:技術的な問題がなければ、内容が十分に役立つものになっているかを見直す

インデックス登録は Google の判断で行われ、監視を始めたからといって登録が保証されるわけではありません。監視の目的は、設定ミスや意図しない noindex のような「直せる問題」に早く気づくことです。スクリプトを動かし始める前に、まずは数件の URL で手動実行して結果を確かめてから cron に登録してください。

まとめ

  • インデックス状況の自動監視には URL 検査 API を使う。ページのインデックス登録レポートの一覧やライブテストは API にない
  • Indexing API は対象が限られており、一般の記事には使わない
  • スクリプトは WordPress の外で、サーバーの cron から動かすと安定しやすい
  • 鍵ファイルはドキュメントルートの外に置き、権限を絞り、Git に含めない
  • 比較には verdict を使い、新しく出た問題だけを通知する
  • 利用上限があるため、新しい記事と更新した記事を優先して少しずつ巡回する

お気軽にご相談ください

お見積りへ お問い合わせへ
▲