Automatisierte SEO-Reports mit n8n und Search Console
Eine tägliche Mail vergleicht zwei Sieben-Tage-Fenster aus der Search Console, zeigt Klicks, Chancen-Keywords und stille Traffic-Einbrüche. Ganz ohne KI.
Wann hast du zuletzt die Google Search Console geöffnet? Bei den meisten lautet die ehrliche Antwort: als der Traffic schon eingebrochen war. Das ist der denkbar schlechteste Zeitpunkt, denn zu diesem Moment hat Google längst wochenlang über deine Seite geurteilt, und du liest nur noch die Quittung.
Ich hab einen kleinen n8n-Workflow gebaut, der das umdreht. Jeden Morgen um sieben bekomme ich eine Mail mit den Zahlen, die zählen, schon verglichen mit der Vorwoche, und dafür reichen sechs Nodes, ganz ohne KI.
Kein KI-Agent, der irgendetwas selbst entscheidet, steckt dahinter, sondern eine feste Kette aus Regeln, die jeden Morgen exakt dasselbe tut. Wer bei n8n noch ganz am Anfang steht, findet in meinen fünf Einsteiger-Workflows für Marketing leichtere Einstiege. Und wer noch zwischen n8n und Make.com schwankt, dem hilft mein Vergleich der beiden weiter.
Was in der Mail landet
Der Report ist eine einzelne HTML-Mail im dunklen Design, ganz ohne Anhang und ohne Login.
Sie beginnt mit der Übersicht: Klicks, Impressionen, Klickrate und durchschnittliche Position der letzten sieben Tage, jeweils mit Prozent-Delta gegenüber den sieben Tagen davor. Bei der Position ist die Farbe umgedreht, weil dort eine kleinere Zahl die gute Richtung ist. Wer nur auf absolute Zahlen schaut, übersieht sonst leicht, dass ein Sprung von Position 8 auf Position 5 ein Erfolg ist, obwohl die Zahl kleiner geworden ist.
Darunter folgen die Keyword-Tabellen. Die Top-Keywords sind nach Impressionen sortiert, nicht nach Klicks, weil die Klickzahlen auf einer kleinen Seite viel zu dünn sind, um irgendetwas sinnvoll zu ranken.
Die Chancen-Tabelle zeigt alles zwischen Position 4 und 20, also vom Ende der ersten bis zur zweiten Ergebnisseite. Genau dort bewegt ein umgeschriebener Title oder ein paar interne Links tatsächlich etwas.
Die versteckten Perlen sind Suchanfragen mit Impressionen, aber null Klicks: Google zeigt dich, und niemand klickt. Das Problem liegt dann meistens am Title oder an der Description, nicht am Ranking. Genau dafür hab ich mir einen Meta-Tag-Checker gebaut, der die Vorschau in Pixeln zeigt statt in Zeichen.
Danach kommt die Kannibalisierung, wenn also dieselbe Suchanfrage mit zwei oder mehr eigenen URLs rankt, aber nur, wenn die zweite URL wirklich einen nennenswerten Anteil der Impressionen abbekommt. Am Ende steht ein Themencluster-Block, der Suchanfragen nach gemeinsamen Wortpaaren gruppiert. Das ist grob, aber es gibt ein brauchbares Bild davon, wofür Google deine Seite hält.
Google Cloud einrichten
Das ist der lästige Teil. Google macht es einem nicht leicht, aber einmalig sind es zwanzig Minuten. Am ehesten stolpert man über den OAuth-Consent-Screen, weil Google dort zwischen internem und externem Nutzertyp unterscheidet und die Bezeichnungen wenig selbsterklärend sind.
- Die Google Cloud Console öffnen und mit dem Konto einloggen, das Zugriff auf die Search-Console-Property hat.
- Ein neues Projekt anlegen.
- Unter APIs & Services → Library die Google Search Console API aktivieren. Soll die Mail über Gmail verschickt werden, zusätzlich die Gmail API aktivieren.
- Den OAuth-Consent-Screen konfigurieren. Externer Nutzertyp reicht. Das eigene Google-Konto unter Testnutzer eintragen, sonst hängt man in der Verifizierungsschleife fest.
- Unter Credentials eine OAuth-Client-ID vom Typ Webanwendung anlegen und n8ns OAuth-Redirect-URL in die autorisierten Redirect-URIs eintragen.
Danach die passenden Credentials in n8n anlegen: eine Google-OAuth2-API-Credential für die Search-Console-Aufrufe, eine Gmail-OAuth2-Credential fürs Versenden. Beide Nodes greifen später über den Namen darauf zu, einmal einrichten reicht. Ohne die App offiziell von Google verifizieren zu lassen, bleibt der Zugriff auf die eingetragenen Testnutzer beschränkt, für den eigenen, einzigen Nutzer im Workflow ist das aber kein Problem.
Der Workflow, Node für Node
1. Schedule Trigger
Ein einfacher täglicher Trigger um 07:00 Uhr. Mehr als die Uhrzeit muss man hier nicht einstellen.
2. Die zwei Zeitfenster berechnen
Das Berechnen der beiden Zeitfenster übernimmt ein Code-Node, die erste Stelle im Workflow, an der die Details wirklich zählen:
const LAG_DAYS = 3;
const WINDOW_DAYS = 7;
const DAY_MS = 86400000;
const iso = (d) => d.toISOString().slice(0, 10);
const base = new Date(Date.now() - LAG_DAYS * DAY_MS);
const curEnd = new Date(Date.UTC(base.getUTCFullYear(), base.getUTCMonth(), base.getUTCDate()));
const curStart = new Date(curEnd.getTime() - (WINDOW_DAYS - 1) * DAY_MS);
const prevEnd = new Date(curStart.getTime() - DAY_MS);
const prevStart = new Date(prevEnd.getTime() - (WINDOW_DAYS - 1) * DAY_MS);
LAG_DAYS = 3 ist die entscheidende Zeile. Search-Console-Daten kommen verzögert an, die letzten zwei bis drei Tage sind praktisch immer unvollständig, und nimmt man sie mit rein, zeigt der Report jeden Morgen einen Rückgang, der gar keiner ist. Mir ist eine Zahl lieber, die drei Tage alt, aber korrekt ist, als eine aktuelle, die es nicht ist. Google selbst markiert die jüngsten Datenpunkte im Performance-Bericht als vorläufig, sie können sich laut Search-Console-Hilfe in den folgenden Stunden noch ändern.
Auf UTC-Mitternacht zu normalisieren ist ebenfalls wichtig, sonst verschieben sich die Fenster je nachdem, wo die n8n-Instanz läuft.
3. Die aktuelle Periode abrufen
Ein HTTP-Request-Node schickt dafür ein POST an:
https://searchconsole.googleapis.com/webmasters/v3/sites/sc-domain:YOUR-DOMAIN/searchAnalytics/query
Die Authentifizierung läuft über einen hinterlegten Credential-Typ, Google OAuth2 API, und der Body kommt als JSON:
{
"startDate": "{{ $json.startDate }}",
"endDate": "{{ $json.endDate }}",
"dimensions": ["query", "page"],
"type": "web",
"aggregationType": "auto",
"rowLimit": 5000,
"startRow": 0
}
Der Request arbeitet mit zwei Dimensionen, query und page. Die Page-Dimension braucht es für die Kannibalisierungsprüfung, denn genau darum geht es dabei: eine Suchanfrage rankt mit mehreren URLs gleichzeitig. Retry on fail mit drei Versuchen einschalten, denn die API wirft gelegentlich einen 429 aus.
4. Die Vorperiode abrufen
Derselbe Node, derselbe Body, andere Daten. Er liest sie aus dem ersten Code-Node zurück, referenziert über den Namen, den man ihm gegeben hat (bei mir heißt er Date window):
{{ $('Date window').first().json.prevStartDate }}
Das sind zwei getrennte Aufrufe statt eines cleveren, denn die API kann keinen Periodenvergleich. Also holt man beide Zeiträume einzeln und rechnet selbst.
5. Den Report bauen
Der zweite Code-Node macht die eigentliche Arbeit. Drei Dinge daraus lohnen sich zum Nachbauen.
Erst auf Suchanfrage-Ebene aggregieren. Die API liefert Query-Page-Paare zurück, ein einzelnes Keyword taucht also mehrfach auf, wenn es mit mehreren URLs rankt. Die werden zu einem Eintrag pro Suchanfrage zusammengefasst, bevor irgendetwas verglichen wird. Klicks und Impressionen addieren sich einfach. Die Position muss dagegen nach Impressionen gewichtet werden, genau so, wie es die Search-Console-Oberfläche selbst rechnet:
entry.position = entry.impressions > 0 ? entry.posWeighted / entry.impressions : 0;
Die Schwellenwerte relativ machen. Meine erste Version hatte feste Grenzwerte, etwa „mindestens 50 Impressionen und keine Klicks“ für die Karte mit den versteckten Perlen. Auf einer Seite meiner Größe war diese Karte an jedem einzelnen Tag leer. Jetzt nimmt sie das 80. Perzentil der eigenen Impressions-Werte:
const gemThreshold = Math.max(2, percentile(curQueries.map((q) => q.impressions), 0.8));
Bei der Kannibalisierung gilt dasselbe Prinzip: Die zweite URL muss mindestens 20 Prozent der Impressionen der Top-URL erreichen, bevor es überhaupt zählt. Relative Schwellenwerte skalieren mit der Seite, statt eine bestimmte Größe vorauszusetzen. Eine Seite mit 50.000 Impressionen pro Woche und eine mit 500 bekommen so beide sinnvolle Grenzwerte, ohne dass am Code etwas geändert werden muss.
Die Themencluster entdoppeln. Suchanfragen nach Wortpaaren zu gruppieren ist billig und funktioniert überraschend gut, aber die Wortpaare eines Keywords überlappen sich stark. „lightroom export“ und „export einstellungen“ erzeugen fast denselben Cluster. Jeder Kandidaten-Cluster wird deshalb mit den bereits gewählten über die Jaccard-Ähnlichkeit verglichen, und alles über 50 Prozent Überlappung fliegt raus:
const jaccard = (a, b) => {
const overlap = [...a].filter((x) => b.has(x)).length;
return overlap / new Set([...a, ...b]).size;
};
Drei Cluster schaffen es in die Mail. Mehr, und die Mail wird zur Wand aus Text.
Der Node liefert am Ende das fertige HTML plus eine Betreffzeile mit den wichtigsten Zahlen, damit die Inbox-Vorschau schon verrät, ob sich überhaupt etwas bewegt hat.
6. Verschicken
Ein Gmail-Node verschickt die Mail im E-Mail-Typ HTML, wobei Betreff und Body aus dem vorherigen Node kommen. „Append attribution“ ausschalten, außer man will n8ns Fußzeile im eigenen Report stehen haben.
Was er tatsächlich gefunden hat
So hab ich gemerkt, dass meine eigene Seite still geworden war. Die Impressionen fielen von rund 150 pro Woche auf 8, und der Report hat mir das mit fertigem Vergleich direkt vor die Nase gesetzt. Ursache war eine Hosting-Migration, die im Stillen sämtliche Redirects der Seite gekillt hatte. So etwas fällt einem beim Blick auf die Startseite nicht auf.
Das ist das eigentliche Argument für einen täglichen statt einen monatlichen Check. An den meisten Morgen tut man nichts damit. Aber in der einen Woche, in der es wirklich zählt, schaut man schon auf den richtigen Bildschirm.
Wo er schwächelt
Die Wortpaar-Cluster sind ein grobes Werkzeug. Sie gruppieren nach Oberflächenform, zwei Suchanfragen mit derselben Bedeutung, aber anderen Wörtern, landen also in verschiedenen Clustern. „seo report automatisieren“ und „automatischer seo bericht“ meinen ungefähr dasselbe, tauchen im Cluster-Block aber getrennt auf. Ein Embedding-Modell würde das besser hinbekommen, aber ich hab bewusst keins eingesetzt, weil das einen kostenlosen Workflow gegen eine API-Rechnung für etwas eintauschen würde, das ich zehn Sekunden am Tag lese.
Er sagt einem nicht, warum sich etwas verändert hat, sondern nur, dass es sich verändert hat, und liefert die URL dazu. Den Rest muss man selbst herausfinden, was bei mir einen Vormittag mit alten URLs und curl gekostet hat.
FAQ
- Warum lässt der Report die letzten drei Tage weg?
- Search-Console-Daten kommen verzögert an. Die letzten zwei bis drei Tage sind unvollständig, und nimmt man sie mit rein, zeigt der Vergleich jeden Morgen einen Rückgang, der gar keiner ist. Der Workflow setzt LAG_DAYS auf 3 und lässt das aktuelle Fenster dort enden.
- Braucht man dafür ein KI-Modell?
- Nein. Alles läuft als reines JavaScript in zwei Code-Nodes. Die Themencluster entstehen über Wortpaare und einen Set-Overlap-Check, nicht über Embeddings. Das hält den Workflow kostenlos und macht jede Zahl nachvollziehbar.
- Funktioniert das auch für eine Seite mit kaum Traffic?
- Ja, genau dafür sind die relativen Schwellenwerte da. Die Karte für versteckte Perlen nimmt das 80. Perzentil der eigenen Impressions-Werte statt eines festen Minimums, sie zeigt also auch dann etwas, wenn das beste Keyword nur zwölf Impressionen hat.
