Google fragen, welche deiner Seiten im Index sind, URL für URL

Christian Drexler

Zwei Tage nachdem ich diese Seite auf einen neuen Stack umgezogen hatte, wollte ich eines wissen: Kennt Google die neuen Seiten überhaupt. Die Search Console konnte es mir nicht sagen. Der Bericht „Seiten“ ist ein Diagramm, das mehrere Tage hinterherhinkt, er fasst Adressen nach Gründen zusammen, und die Liste hinter jedem Grund ist gekappt und nur eine Stichprobe. Für eine Seite, deren URLs sich in der Woche davor alle geändert hatten, beschrieb der Bericht noch die alte.

Es gibt eine API, die die Frage direkt beantwortet. Man gibt ihr eine Adresse, und sie sagt, was Googles Index in diesem Moment über diese Adresse weiß. In einer Schleife über die Sitemap ergibt das das echte Bild.

Der Aufruf

Der Endpunkt ist die URL Inspection API, ein Teil der Search Console API. Sie nimmt die Adresse, um die es geht, und die Property, zu der sie gehört.

from googleapiclient.discovery import build

svc = build("searchconsole", "v1", credentials=creds, cache_discovery=False)
res = svc.urlInspection().index().inspect(body={
    "inspectionUrl": "https://example.com/blog/some-post",
    "siteUrl": "sc-domain:example.com",
}).execute()

status = res["inspectionResult"]["indexStatusResult"]
print(status.get("coverageState"), status.get("lastCrawlTime"))

siteUrl ist die Property genau so, wie die Search Console sie nennt. Eine Domain-Property heißt sc-domain:example.com, eine URL-Präfix-Property ist die volle Adresse mit Schrägstrich am Ende. Stimmt diese Zeichenkette nicht, scheitert jeder Aufruf mit einem Berechtigungsfehler, der nach einem Anmeldeproblem klingt und keines ist.

Die Felder, die ich aus der Antwort wirklich lese:

  • coverageState: der Satz, den man aus der Oberfläche kennt, etwa „Submitted and indexed“, „Crawled - currently not indexed“ oder „URL is unknown to Google“.
  • verdict: PASS, NEUTRAL oder FAIL, praktisch zum Zählen.
  • lastCrawlTime: wann der Googlebot die Seite zuletzt geholt hat. Leer heißt nie.
  • userCanonical und googleCanonical: die Canonical-Adresse, die man selbst angegeben hat, und die, die Google gewählt hat. Weichen sie voneinander ab, hält Google die Seite für die Kopie einer anderen.

Zugangsdaten ohne Browser

Für ein Skript, das unbeaufsichtigt läuft, ist ein Dienstkonto der bequeme Weg. Man legt es in einem Google-Cloud-Projekt an, aktiviert dort die Search Console API, lädt die Schlüsseldatei herunter und fügt dann die E-Mail-Adresse des Dienstkontos in der Search Console als Nutzer der Property hinzu, unter Einstellungen, Nutzer und Berechtigungen. Lesezugriff reicht für die Abfrage.

Diesen letzten Schritt lassen viele aus. Das Dienstkonto ist eine eigene Identität. Es sieht nichts, bis der Inhaber der Property es einlädt, genau wie ein Kollege nichts sähe.

from google.oauth2 import service_account

creds = service_account.Credentials.from_service_account_file(
    "gsc-key.json",
    scopes=["https://www.googleapis.com/auth/webmasters.readonly"],
)

Über die ganze Sitemap

Die Sitemap ist bereits die Liste der Adressen, die im Index stehen sollen, also ist sie die natürliche Eingabe. Die <loc>-Einträge holt ein regulärer Ausdruck, die Abfragen laufen in einem kleinen Thread-Pool.

import re, urllib.request
from collections import Counter
from concurrent.futures import ThreadPoolExecutor

SITE, SITEMAP = "sc-domain:example.com", "https://example.com/sitemap.xml"
urls = re.findall(r"<loc>([^<]+)", urllib.request.urlopen(SITEMAP).read().decode())

def inspect(u):
    svc = build("searchconsole", "v1", credentials=creds, cache_discovery=False)
    try:
        r = svc.urlInspection().index().inspect(
            body={"inspectionUrl": u, "siteUrl": SITE}).execute()
        s = r["inspectionResult"]["indexStatusResult"]
        return u, s.get("coverageState"), s.get("userCanonical"), s.get("googleCanonical")
    except Exception as e:
        return u, f"ERROR {e}"[:120], None, None

with ThreadPoolExecutor(10) as ex:
    rows = list(ex.map(inspect, urls))

print(Counter(r[1] for r in rows))
for u, state, mine, theirs in rows:
    if state != "Submitted and indexed":
        print(state, "|", u)
    elif mine and theirs and mine.rstrip("/") != theirs.rstrip("/"):
        print("canonical differs |", u, "->", theirs)

Jeder Worker baut seinen eigenen Client, weil sich das Client-Objekt nicht sicher zwischen Threads teilen lässt. Zehn Worker schaffen etwa 160 Adressen in zwei Minuten.

Das Kontingent liegt bei 2.000 Abfragen pro Property und Tag und 600 pro Minute. Für eine Seite mit ein paar hundert Unterseiten reicht das bequem für einen täglichen Lauf. Für eine Seite mit fünfzigtausend ist es eine Stichprobe, und man würde die Seiten auswählen, auf die es ankommt.

Was es mir gezeigt hat

Der erste Lauf, zwei Tage nach dem Umzug, gab für 154 Adressen das hier aus: 153-mal „URL is unknown to Google“, einmal „Submitted and indexed“. Die eine indexierte Seite war die Startseite, und ihr letztes Crawl-Datum lag vor dem Umzug. Google hielt noch die alte Seite und hatte die neue überhaupt nicht angesehen.

Der Grund stand im Sitemap-Eintrag der Search Console. Er war vor Jahren eingereicht worden, und der letzte erfolgreiche Abruf lag Monate zurück. Google hatte keinen Anlass wiederzukommen. Ich habe die Sitemap neu eingereicht, was ein weiterer API-Aufruf ist, und die Liste am selben Tag über IndexNow an Bing gegeben.

Dreieinhalb Wochen später gab dasselbe Skript 152 von 156 als indexiert aus. Zwei Werkzeug-Seiten standen auf „Crawled - currently not indexed“. So sagt Google, dass es die Seite gelesen hat und nicht überzeugt war. Zwei Adressen eines ganz neuen Beitrags waren noch unbekannt. Mit dieser Liste kann ich arbeiten: zwei Seiten verbessern, auf zwei warten. Das Diagramm in der Oberfläche hätte mir eine grüne Linie gezeigt, die nach oben geht.

Die drei Zustände, auf die es ankommt

URL is unknown to Google. Google hat diese Adresse nie abgerufen. Die Seite umzuschreiben ändert nichts, weil niemand sie gelesen hat. Das ist ein Problem der Auffindbarkeit: prüfen, ob die Adresse in der Sitemap steht, ob die Sitemap kürzlich abgerufen wurde und ob mindestens eine andere Seite auf sie verlinkt.

Discovered - currently not indexed. Google kennt die Adresse und hat sie noch nicht abgerufen. Bei einer kleinen Seite löst sich das meist binnen Tagen. Bleibt es, zeigen zu wenige interne Links auf die Seite.

Crawled - currently not indexed. Google hat die Seite abgerufen und entschieden, sie nicht zu behalten. Hier geht es um Substanz. Vergleiche die Seite mit ihren Geschwistern, die es in den Index geschafft haben. Meist ist sie kürzer, oder einer anderen Seite sehr ähnlich, oder es steht wenig Eigenes darauf.

Wo es sich bezahlt macht

Der Durchlauf hilft vor allem in drei Situationen.

Nach einem Umzug oder Relaunch. Man erfährt binnen Tagen, ob die neuen Adressen aufgenommen werden, lange bevor der Bericht nachzieht.

Wenn die Search Console die Canonical-Mail schickt. „Duplikat, Google hat eine andere Seite als der Nutzer als kanonische Seite bestimmt“ kommt ohne die Liste der Seiten. Der Durchlauf gibt jede Adresse aus, bei der die beiden Canonicals voneinander abweichen. Bei einer anderen meiner Seiten zeigte er auf zwei übersetzte Seiten, deren Text noch in der Ausgangssprache war. Google hatte recht.

Als wöchentliche Gewohnheit. Einmal pro Woche die Zustände zählen und die Zahlen ablegen ergibt einen Verlauf, den die Oberfläche in dieser Form nicht aufbewahrt.

Der Durchlauf liest nur und ändert nichts daran, wie Google die Seite sieht. Er ersetzt eine Vermutung durch eine Liste. Meistens ist das der Teil, der gefehlt hat.