// SEO

NawiSEO 5 - top 3 frazy

▸ // rozdziały lekcji

NawiSEO 5 - Wolniś przy podium z trzema frazami

Search Console pokaże ci frazy, na które strona wyskakuje w Google. Pokaże też podstrony, które zbierają kliknięcia. Tyle że osobno. Żeby dla jednej podstrony zobaczyć jej najlepsze zapytania, klikasz filtr strony, przełączasz zakładkę, spisujesz trzy frazy i robisz to samo dla następnego adresu. Przy 200 podstronach to dzień przeklikiwania, a za miesiąc dane są nieaktualne.

Ten workflow robi to jednym przebiegiem. Pobiera pary „strona + fraza" z ostatnich trzech pełnych miesięcy, grupuje je po adresie i dla każdego zapisuje trzy najlepsze frazy: najpierw po kliknięciach, a przy remisie po wyświetleniach.

Co robi ten workflow

  1. Uruchom recznie - start przyciskiem Execute workflow.
  2. Ustawienia - trzyma adres właściwości w Search Console i liczy zakres dat od i do.
  3. Przygotuj tabele - Data Table z operacją Create zakłada tabelę nawiseo_gsc_slowa, jeśli jej jeszcze nie ma.
  4. Frazy z Search Console - HTTP Request wysyła zapytanie do API Search Analytics z wymiarami page i query.
  5. Top 3 dla adresu - węzeł Code grupuje wiersze po adresie, układa frazy po kliknięciach i wyświetleniach i zostawia trzy najlepsze.
  6. Zapisz frazy - Data Table z operacją Upsert zapisuje wynik, dopasowując wiersz po url i miejsce.

Czego potrzebujesz

  • poświadczenia Google OAuth2 API w n8n, tego samego co w lekcji 3. Zakres https://www.googleapis.com/auth/webmasters.readonly i włączone Search Console API w Google Cloud,
  • właściwości w Search Console, do której to konto ma dostęp,
  • niczego więcej. Tabelę workflow zakłada sam.

Konfigurację Google krok po kroku pokazuję w lekcji Credentials Google OAuth2 na komputerze.

Ten workflow nie czyta tabeli nawiseo_adresy. Bierze adresy prosto z Search Console, więc w wyniku są tylko te podstrony, które w tym okresie choć raz wyświetliły się w Google.

Jak uruchomić

  1. Zaimportuj plik z tej lekcji. Jak to zrobić, pokazuję w lekcji Jak importować workflow.
  2. Otwórz węzeł Ustawienia i w polu wlasciwosc wpisz swoją właściwość dokładnie tak, jak jest w Search Console: prefiks URL z ukośnikiem na końcu, na przykład https://twojadomena.pl/, albo sc-domain:twojadomena.pl dla właściwości domenowej.
  3. Otwórz węzeł Frazy z Search Console i wybierz swoje poświadczenie Google.
  4. Kliknij Execute workflow.
  5. Wejdź w Overview, zakładka Data tables, i otwórz nawiseo_gsc_slowa.

Węzeł Ustawienia z polami wlasciwosc, od i do oraz podglądem wyliczonych dat

Zakres dat liczy się sam

W węźle Ustawienia są dwa wyrażenia:

od: {{ $now.minus({months: 3}).startOf('month').toFormat('yyyy-MM-dd') }}
do: {{ $now.minus({months: 1}).endOf('month').toFormat('yyyy-MM-dd') }}

od to pierwszy dzień miesiąca sprzed trzech miesięcy, do to ostatni dzień poprzedniego miesiąca. Odpalasz 24 września, dostajesz czerwiec, lipiec i sierpień. Bieżący, niedomknięty miesiąc nie wpada, więc wynik nie zależy od tego, którego dnia klikniesz.

Trzy miesiące wyrównują wahania. Jedna fraza, która raz wystrzeliła w tygodniu, nie wypchnie tych, na które ludzie klikają regularnie.

Zapytanie do API

Węzeł HTTP Request wysyła POST na adres searchAnalytics/query właściwości. Adres właściwości przechodzi przez encodeURIComponent, bo ukośniki i dwukropek z https:// albo sc-domain: muszą być zakodowane w ścieżce.

Treść zapytania:

{{ JSON.stringify({ startDate: $('Ustawienia').item.json.od, endDate: $('Ustawienia').item.json.do, dimensions: ['page','query'], rowLimit: 25000 }) }}

Daty idą z węzła Ustawienia. Dwa wymiary znaczą, że każdy wiersz odpowiedzi to para: adres i fraza, z kliknięciami, wyświetleniami i średnią pozycją. rowLimit ustawiłem na 25000. Bez niego API zwraca domyślnie najwyżej 1000 wierszy, a przy parach strona plus fraza większy serwis zjada ten limit szybko i część adresów dostaje niepełną listę.

Węzeł Code, linijka po linijce

API oddaje płaską listę par. Tak ją zamieniamy na trzy frazy dla każdego adresu:

const okres = $('Ustawienia').first().json.od + ' - ' + $('Ustawienia').first().json.do;
const wiersze = $input.first().json.rows || [];
const poAdresie = {};

for (const w of wiersze) {
  const [url, fraza] = w.keys;
  (poAdresie[url] ??= []).push({ fraza, klikniecia: w.clicks, wyswietlenia: w.impressions, pozycja: Math.round(w.position * 10) / 10 });
}

const wynik = [];
for (const [url, frazy] of Object.entries(poAdresie)) {
  frazy.sort((a, b) => b.klikniecia - a.klikniecia || b.wyswietlenia - a.wyswietlenia);
  frazy.slice(0, 3).forEach((f, i) => wynik.push({ json: { url, miejsce: i + 1, ...f, okres } }));
}
return wynik;

Po kolei:

  • okres to tekst typu „2026-06-01 - 2026-08-31". Trafia do każdego wiersza, żebyś wiedział, z jakiego okna pochodzą dane,
  • rows || [] zabezpiecza przed pustą odpowiedzią. Właściwość bez ruchu nie ma pola rows i bez tego kod by się wysypał,
  • w.keys to lista wartości wymiarów w tej samej kolejności, w jakiej je podałeś w zapytaniu. Najpierw page, potem query,
  • ??= zakłada pustą listę dla adresu, który pojawia się pierwszy raz,
  • Math.round(w.position * 10) / 10 zaokrągla średnią pozycję do jednego miejsca po przecinku,
  • sort układa frazy malejąco po kliknięciach. Przy remisie, także przy samych zerach, decydują wyświetlenia - to robi || w środku sortowania. slice(0, 3) zostawia trzy pierwsze. miejsce dostaje numer 1, 2 albo 3.

Kod zwraca osobny item dla każdej frazy. Adres z trzema frazami daje trzy items, adres z jedną frazą daje jeden.

Dlaczego frazy bez kliknięć zostają? Pierwsza wersja tego workflow je pomijała i na mojej stronie zwróciła pustą tabelę. Search Console ukrywa część zapytań, więc przy mniejszym ruchu kliknięcia na poziomie pojedynczej frazy często wychodzą zerowe, choć cała podstrona kliknięcia ma. Wyświetlenia też są sygnałem: Google już łączy tę frazę z podstroną. Fraza z 90 wyświetleniami na pozycji 6 to kandydat do poprawki title, zanim zacznie przynosić wejścia.

Okno węzła Top 3 dla adresu - po lewej odpowiedź Search Console z parami adres i fraza, w środku kod, po prawej 93 wyniki w widoku tabeli

Zapis: upsert po dwóch kolumnach

Węzeł Zapisz frazy szuka wiersza, w którym zgadzają się jednocześnie url i miejsce. Jest - aktualizuje. Nie ma - dopisuje. Dzięki temu tabela ma najwyżej trzy wiersze na adres, a kolejne uruchomienie za miesiąc nadpisuje stare frazy nowymi zamiast dokładać.

Na damianslimak.pl za czerwiec-sierpień 2026 Search Console oddał 154 pary adres-fraza dla 49 podstron. Po grupowaniu do tabeli trafiły 93 frazy.

Tabela nawiseo_gsc_slowa po przebiegu - adres, miejsce 1-3, fraza, kliknięcia, wyświetlenia i średnia pozycja

Pro-tip. Zestaw tę tabelę z nawiseo_adresy z lekcji 2. Jeśli fraza z miejsca 1 nie występuje w title podstrony, masz kandydata do poprawki numer jeden.

Haczyk. Upsert nadpisuje, ale nie kasuje. Podstrona miała w maju trzy frazy z kliknięciami, a teraz ma dwie? Wiersz z miejsce 3 zostaje ze starymi danymi. Poznasz go po kolumnie okres, która nie zgadza się z resztą. Chcesz czystą tabelę - przed uruchomieniem ją wyczyść.

Haczyk. Zła wartość wlasciwosc kończy się błędem uprawnień, choć konto ma pełny dostęp. Najczęstsza przyczyna: właściwość domenowa wpisana jako adres z https:// albo brak ukośnika na końcu przy prefiksie URL.

Kiedy tego nie używać

Na stronie, która dopiero wystartowała i nie ma jeszcze wyświetleń w Google. Search Console nie odda wtedy żadnych par i tabela zostanie pusta. Zaczekaj kilka tygodni po publikacji.

Nie nadaje się też do śledzenia pozycji dzień po dniu. Bierze trzy pełne miesiące naraz. Do sprawdzania zmian tuż po publikacji potrzebujesz krótszego okna.

Podsumowanie

Jeden przebieg zamienia płaską listę par „strona + fraza" w tabelę, w której każda podstrona ma swoje trzy najmocniejsze zapytania. Zakres dat liczy się sam, frazy bez kliknięć odpadają, a upsert po url i miejsce pilnuje, żeby tabela nie puchła.

W następnej lekcji, NawiSEO 6 - PageSpeed Insights, dopiszemy do tabeli adresów wynik wydajności na telefonie i komputerze.

Pokaż JSON do skopiowania - nawiseo-5-search-console-top-slowa.json
{
  "name": "NawiSEO 5 - Search Console, 3 najlepsze frazy dla adresu",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "994f1b51-8a18-a751-7729-a09aa49cb790",
      "name": "Uruchom recznie"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "0adede4f-9cb1-cff9-760d-1366e422b375",
              "name": "wlasciwosc",
              "value": "https://damianslimak.pl/",
              "type": "string"
            },
            {
              "id": "22dbad68-8838-b55f-9645-d3a92ac008db",
              "name": "od",
              "value": "={{ $now.minus({months: 3}).startOf('month').toFormat('yyyy-MM-dd') }}",
              "type": "string"
            },
            {
              "id": "b14419b7-54b4-39ba-52bb-a1606eca3566",
              "name": "do",
              "value": "={{ $now.minus({months: 1}).endOf('month').toFormat('yyyy-MM-dd') }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ],
      "id": "41e3753c-36c4-ed18-2304-2f49742c002a",
      "name": "Ustawienia"
    },
    {
      "parameters": {
        "resource": "table",
        "operation": "create",
        "tableName": "nawiseo_gsc_slowa",
        "columns": {
          "column": [
            {
              "name": "url",
              "type": "string"
            },
            {
              "name": "miejsce",
              "type": "number"
            },
            {
              "name": "fraza",
              "type": "string"
            },
            {
              "name": "klikniecia",
              "type": "number"
            },
            {
              "name": "wyswietlenia",
              "type": "number"
            },
            {
              "name": "pozycja",
              "type": "number"
            },
            {
              "name": "okres",
              "type": "string"
            }
          ]
        },
        "options": {
          "createIfNotExists": true
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        440,
        0
      ],
      "id": "cb43232d-6450-ff79-0516-0c2048628e67",
      "name": "Przygotuj tabele"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://www.googleapis.com/webmasters/v3/sites/{{ encodeURIComponent($('Ustawienia').item.json.wlasciwosc) }}/searchAnalytics/query",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ startDate: $('Ustawienia').item.json.od, endDate: $('Ustawienia').item.json.do, dimensions: ['page','query'], rowLimit: 25000 }) }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        660,
        0
      ],
      "id": "5b66dc95-c1ef-c13a-7e76-2ed32a635aa9",
      "name": "Frazy z Search Console"
    },
    {
      "parameters": {
        "jsCode": "// Grupuje frazy po adresie i zostawia 3 najlepsze: po kliknięciach, przy remisie po wyświetleniach.\nconst okres = $('Ustawienia').first().json.od + ' - ' + $('Ustawienia').first().json.do;\nconst wiersze = $input.first().json.rows || [];\nconst poAdresie = {};\n\nfor (const w of wiersze) {\n  const [url, fraza] = w.keys;\n  (poAdresie[url] ??= []).push({ fraza, klikniecia: w.clicks, wyswietlenia: w.impressions, pozycja: Math.round(w.position * 10) / 10 });\n}\n\nconst wynik = [];\nfor (const [url, frazy] of Object.entries(poAdresie)) {\n  frazy.sort((a, b) => b.klikniecia - a.klikniecia || b.wyswietlenia - a.wyswietlenia);\n  frazy.slice(0, 3).forEach((f, i) => wynik.push({ json: { url, miejsce: i + 1, ...f, okres } }));\n}\nreturn wynik;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        0
      ],
      "id": "98622fbc-2ac8-88bf-4815-5acfc99ecc28",
      "name": "Top 3 dla adresu"
    },
    {
      "parameters": {
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "nawiseo_gsc_slowa",
          "mode": "name"
        },
        "matchType": "allConditions",
        "filters": {
          "conditions": [
            {
              "keyName": "url",
              "condition": "eq",
              "keyValue": "={{ $json.url }}"
            },
            {
              "keyName": "miejsce",
              "condition": "eq",
              "keyValue": "={{ $json.miejsce }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "url": "={{ $json.url }}",
            "miejsce": "={{ $json.miejsce }}",
            "fraza": "={{ $json.fraza }}",
            "klikniecia": "={{ $json.klikniecia }}",
            "wyswietlenia": "={{ $json.wyswietlenia }}",
            "pozycja": "={{ $json.pozycja }}",
            "okres": "={{ $json.okres }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "url",
              "displayName": "url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "miejsce",
              "displayName": "miejsce",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "fraza",
              "displayName": "fraza",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "klikniecia",
              "displayName": "klikniecia",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "wyswietlenia",
              "displayName": "wyswietlenia",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "pozycja",
              "displayName": "pozycja",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "okres",
              "displayName": "okres",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        1100,
        0
      ],
      "id": "308c2113-1ba1-2ee8-359d-a036bcab84b3",
      "name": "Zapisz frazy"
    }
  ],
  "connections": {
    "Uruchom recznie": {
      "main": [
        [
          {
            "node": "Ustawienia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ustawienia": {
      "main": [
        [
          {
            "node": "Przygotuj tabele",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Przygotuj tabele": {
      "main": [
        [
          {
            "node": "Frazy z Search Console",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Frazy z Search Console": {
      "main": [
        [
          {
            "node": "Top 3 dla adresu",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Top 3 dla adresu": {
      "main": [
        [
          {
            "node": "Zapisz frazy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}