// SEO

NawiSEO 1 - parser sitemap

▸ // rozdziały lekcji

NawiSEO 1 - Wolniś rozwija mapę strony z 235 adresami

Każdy audyt SEO zaczyna się od tego samego pytania: jakie adresy w ogóle ma ta strona? Na sklepie z 3000 produktów ręczne przeklikiwanie mapy strony to popołudnie wyrzucone do kosza. Kopiujesz z przeglądarki, wklejasz do arkusza i gubisz połowę, bo mapa jest podzielona na osiem plików.

Ten workflow robi to jednym kliknięciem. Podajesz adres mapy, dostajesz tabelę z kompletem adresów i informacją, z której podmapy każdy pochodzi.

To pierwsza lekcja serii NawiSEO. Kolejne workflow pracują na liście adresów, którą budujesz właśnie tutaj.

Co robi ten workflow

  1. Uruchom recznie - start kliknięciem.
  2. Ustawienia - jedno pole sitemap z adresem mapy. Tu wpisujesz swoją.
  3. Przygotuj tabele - zakłada tabelę nawiseo_adresy w Data Tables albo używa istniejącej.
  4. Pobierz sitemap - ściąga plik XML.
  5. XML na JSON - zamienia XML na dane, które n8n umie czytać.
  6. Czy to indeks map? - sprawdza, czy plik jest indeksem map (sitemapindex), czy zwykłą mapą z adresami (urlset).
  7. Lista podmap, Rozbij podmapy, Pobierz podmape, XML podmapy, Adresy z podmapy - gałąź dla indeksu. Wyciąga adresy podmap, pobiera każdą osobno i zbiera z niej adresy stron.
  8. Adresy z mapy - gałąź dla zwykłej mapy. Zbiera adresy od razu.
  9. Rozbij adresy - zamienia listę adresów na osobne items, jeden adres to jeden item.
  10. Zapisz do tabeli - upsert po kolumnie url. Zapisuje adres, domenę i podmapę, z której przyszedł.

Cała kanwa po uruchomieniu, widoczne rozgałęzienie na węźle Czy to indeks map? i liczby items na połączeniach

Czego potrzebujesz

  • n8n z dostępem do Data Tables (Overview > zakładka Data tables),
  • adresu mapy twojej strony.

Nic więcej. Żadnego konta Google, żadnego klucza. Tabela siedzi w tej samej instancji n8n co workflow.

Nie wiesz, gdzie jest mapa? Zajrzyj do pliku robots.txt na swojej domenie, tam zwykle jest linia Sitemap: z adresem. WordPress trzyma ją najczęściej pod /sitemap_index.xml albo /wp-sitemap.xml.

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 sitemap wpisz adres swojej mapy. W pliku stoi https://damianslimak.pl/sitemap-index.xml.
  3. Kliknij Execute workflow.
  4. Wejdź w Overview > zakładka Data tables i otwórz tabelę nawiseo_adresy.

Tabela ma od razu 18 własnych kolumn, choć ten workflow wypełnia tylko trzy: url, domena i sitemap. Resztę dopiszą kolejne lekcje serii. Każdy workflow NawiSEO zakłada tabelę z pełnym zestawem kolumn, więc możesz przerabiać lekcje w dowolnej kolejności i schemat się nie rozjedzie.

Tabela nawiseo_adresy w zakładce Data tables, wypełnione kolumny url, domena, sitemap

Tabela, która zakłada się sama

Węzeł Przygotuj tabele to Data Table z zasobem Table i operacją Create. Ma włączoną opcję Reuse Existing Tables. Dzięki niej pierwsze uruchomienie zakłada tabelę, a każde następne używa tej, która już jest.

Haczyk. Sprawdziłem to w testach: jeśli opcja Reuse Existing Tables nie jest jawnie włączona w parametrach węzła, drugie uruchomienie kończy się błędem „Data table with name 'nawiseo_adresy' already exists in this project". W pliku z tej lekcji jest włączona. Budujesz własny workflow na tym wzorze? Włącz ją od razu.

Po tym węźle w danych płynie wynik operacji na tabeli, a nie pole sitemap. Dlatego Pobierz sitemap nie bierze adresu z $json, tylko sięga do węzła Ustawienia:

{{ $('Ustawienia').item.json.sitemap }}

Pozostałe węzły Data Table odwołują się do tabeli po nazwie (tryb „By Name"). Nie musisz niczego wybierać z listy po imporcie.

Jedna podmapa, czyli haczyk z listą

Węzeł XML ma jedną nieprzyjemną cechę. Gdy w pliku jest kilka elementów sitemap albo url, dostajesz listę. Gdy jest jeden, dostajesz pojedynczy obiekt. Wyrażenie, które robi .map() na liście, na obiekcie się wywala.

Trafiłem na to przy teście na damianslimak.pl: indeks map ma tam jedną podmapę. Dlatego w węźle Lista podmap wyrażenie wygląda tak:

{{ [].concat($json.sitemapindex.sitemap).map(s => s.loc) }}

[].concat(...) zawsze zwraca listę. Z listy robi listę, z pojedynczego obiektu listę z jednym elementem. Potem .map(s => s.loc) wyciąga z każdego elementu sam adres. Ten sam zapis siedzi w Adresy z podmapy i Adresy z mapy, tam na $json.urlset.url.

Pro-tip. Ten wzór przyda ci się wszędzie, gdzie czytasz XML albo API, które raz oddaje listę, a raz obiekt. Workflow z kanałem YouTube w dziale z gotowymi workflow robi dokładnie to samo z listą filmów.

Upsert, czyli bez duplikatów

Zapisz do tabeli używa operacji upsert z warunkiem url równe adresowi z bieżącego itemu. Jeśli wiersz z takim adresem już jest, zostaje zaktualizowany. Jeśli go nie ma, powstaje nowy.

Test na damianslimak.pl dał 235 adresów. Drugie uruchomienie nie dopisało ani jednego duplikatu, wciąż było 235 wierszy. Możesz więc odpalać workflow co tydzień i tabela będzie dopisywać tylko nowe podstrony.

Kolumnę domena wylicza wyrażenie {{ $json.url.split('/')[2] }}. Adres dzieli się po ukośnikach, a trzeci kawałek to domena.

Węzeł Zapisz do tabeli, operacja Upsert, warunek url i mapowanie trzech kolumn

Haczyk. Workflow schodzi jeden poziom w dół: indeks, a pod nim zwykłe mapy. Jeśli twój indeks wskazuje na kolejny indeks, ta konstrukcja go nie rozwinie. Przy dużych sklepach otwórz indeks w przeglądarce i sprawdź strukturę, zanim uznasz wynik za pełny.

W starej wersji tej lekcji zapis szedł do Google Sheets, a węzeł „Create spreadsheet" przy każdym uruchomieniu zakładał pusty arkusz, do którego nic nie trafiało. Razem z arkuszem zniknął ten błąd.

Czego ten workflow nie zobaczy

Mapa strony pokazuje to, co strona deklaruje. Nie to, co faktycznie istnieje. Osierocone podstrony, adresy bez linków wewnętrznych albo strony, które wypadły z mapy przez błąd wtyczki, znajdzie tylko crawler chodzący po linkach.

Adresów grafik z map obrazków też tu nie będzie. Wyrażenia biorą tylko loc z elementów url, a adresy obrazków siedzą głębiej.

Podsumowanie

Z jednego adresu mapy robisz tabelę z pełną listą podstron, bez arkusza i bez kluczy. Tabela zakłada się sama, upsert pilnuje, żeby nic się nie dublowało, a [].concat(...) ratuje przed mapą z jedną podmapą.

W NawiSEO 2 - audyt title, description i H1 wejdziemy na każdy z tych adresów i sprawdzimy meta tagi.

Pokaż JSON do skopiowania - nawiseo-1-parser-sitemap-xml.json
{
  "name": "NawiSEO 1 - Parser sitemap XML",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "4d6dc158-29a0-196d-4b53-f84d7bbaa987",
      "name": "Uruchom recznie"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "9c4b98fe-940a-7184-519e-e60f4993db16",
              "name": "sitemap",
              "value": "https://damianslimak.pl/sitemap-index.xml",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        220,
        0
      ],
      "id": "b22fcabf-3739-5e22-1ad1-d52b7175a5ed",
      "name": "Ustawienia"
    },
    {
      "parameters": {
        "resource": "table",
        "operation": "create",
        "tableName": "nawiseo_adresy",
        "columns": {
          "column": [
            {
              "name": "url",
              "type": "string"
            },
            {
              "name": "domena",
              "type": "string"
            },
            {
              "name": "sitemap",
              "type": "string"
            },
            {
              "name": "status_http",
              "type": "number"
            },
            {
              "name": "title",
              "type": "string"
            },
            {
              "name": "title_dl",
              "type": "number"
            },
            {
              "name": "description",
              "type": "string"
            },
            {
              "name": "description_dl",
              "type": "number"
            },
            {
              "name": "h1",
              "type": "string"
            },
            {
              "name": "h1_ilosc",
              "type": "number"
            },
            {
              "name": "sprawdzono",
              "type": "date"
            },
            {
              "name": "nowy_title",
              "type": "string"
            },
            {
              "name": "nowy_description",
              "type": "string"
            },
            {
              "name": "nowy_h1",
              "type": "string"
            },
            {
              "name": "gsc_status",
              "type": "string"
            },
            {
              "name": "gsc_ostatni_crawl",
              "type": "string"
            },
            {
              "name": "psi_mobile",
              "type": "number"
            },
            {
              "name": "psi_desktop",
              "type": "number"
            }
          ]
        },
        "options": {
          "createIfNotExists": true
        }
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        440,
        0
      ],
      "id": "49cb4755-0849-7903-a799-a2eae13d672d",
      "name": "Przygotuj tabele"
    },
    {
      "parameters": {
        "url": "={{ $('Ustawienia').item.json.sitemap }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        660,
        0
      ],
      "id": "e55caadd-ec04-5e10-9f5b-dabebd50f3ed",
      "name": "Pobierz sitemap"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.xml",
      "typeVersion": 1,
      "position": [
        880,
        0
      ],
      "id": "7b8645ae-d105-fa2d-afb2-2b3af5324123",
      "name": "XML na JSON"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 3
          },
          "conditions": [
            {
              "id": "a1",
              "leftValue": "={{ $json.sitemapindex }}",
              "rightValue": "",
              "operator": {
                "type": "object",
                "operation": "exists",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.3,
      "position": [
        1100,
        0
      ],
      "id": "5dcb6178-21e4-9669-1cf1-91733166f8fd",
      "name": "Czy to indeks map?"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c79c572b-3d51-5ca8-e21a-8a3657dca990",
              "name": "podmapy",
              "value": "={{ [].concat($json.sitemapindex.sitemap).map(s => s.loc) }}",
              "type": "array"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1320,
        -120
      ],
      "id": "b0523ac8-944c-1118-4cc8-155abb15eaf5",
      "name": "Lista podmap"
    },
    {
      "parameters": {
        "fieldToSplitOut": "podmapy",
        "options": {
          "destinationFieldName": "podmapa"
        }
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        1540,
        -120
      ],
      "id": "922f4f05-c278-95c1-028e-7221200bd6c7",
      "name": "Rozbij podmapy"
    },
    {
      "parameters": {
        "url": "={{ $json.podmapa }}",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1760,
        -120
      ],
      "id": "1c0b59fd-b712-0f68-b765-50ad7e5301c4",
      "name": "Pobierz podmape"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.xml",
      "typeVersion": 1,
      "position": [
        1980,
        -120
      ],
      "id": "5ea594ae-86d0-a48e-476c-f7c1a96a1685",
      "name": "XML podmapy"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "703b66b8-a00e-7e88-5d08-2ae6469a22d0",
              "name": "adresy",
              "value": "={{ [].concat($json.urlset.url).map(u => u.loc) }}",
              "type": "array"
            },
            {
              "id": "91d30bbf-2186-ca14-2f8c-f7ded7a6b840",
              "name": "sitemap",
              "value": "={{ $('Rozbij podmapy').item.json.podmapa }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2200,
        -120
      ],
      "id": "c5fd4399-1390-a780-b1dc-471596fe72f1",
      "name": "Adresy z podmapy"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "77bd6ac0-3dd5-88ce-1352-60a12b6a3355",
              "name": "adresy",
              "value": "={{ [].concat($json.urlset.url).map(u => u.loc) }}",
              "type": "array"
            },
            {
              "id": "5d3753a6-c722-943a-2899-586eca92786a",
              "name": "sitemap",
              "value": "={{ $('Ustawienia').item.json.sitemap }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1320,
        120
      ],
      "id": "529728f5-ebe6-7846-21f5-0d079909f1c1",
      "name": "Adresy z mapy"
    },
    {
      "parameters": {
        "fieldToSplitOut": "adresy",
        "include": "selectedOtherFields",
        "fieldsToInclude": "sitemap",
        "options": {
          "destinationFieldName": "url"
        }
      },
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        2420,
        0
      ],
      "id": "d12ce131-1f23-3a45-9dbe-65f94d77b3cd",
      "name": "Rozbij adresy"
    },
    {
      "parameters": {
        "operation": "upsert",
        "dataTableId": {
          "__rl": true,
          "value": "nawiseo_adresy",
          "mode": "name"
        },
        "filters": {
          "conditions": [
            {
              "keyName": "url",
              "condition": "eq",
              "keyValue": "={{ $json.url }}"
            }
          ]
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "url": "={{ $json.url }}",
            "domena": "={{ $json.url.split('/')[2] }}",
            "sitemap": "={{ $json.sitemap }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "url",
              "displayName": "url",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "domena",
              "displayName": "domena",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "sitemap",
              "displayName": "sitemap",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": false
            },
            {
              "id": "status_http",
              "displayName": "status_http",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "title",
              "displayName": "title",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "title_dl",
              "displayName": "title_dl",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "description",
              "displayName": "description",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "description_dl",
              "displayName": "description_dl",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "h1",
              "displayName": "h1",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "h1_ilosc",
              "displayName": "h1_ilosc",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "sprawdzono",
              "displayName": "sprawdzono",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "date",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "nowy_title",
              "displayName": "nowy_title",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "nowy_description",
              "displayName": "nowy_description",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "nowy_h1",
              "displayName": "nowy_h1",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "gsc_status",
              "displayName": "gsc_status",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "gsc_ostatni_crawl",
              "displayName": "gsc_ostatni_crawl",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "psi_mobile",
              "displayName": "psi_mobile",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": true
            },
            {
              "id": "psi_desktop",
              "displayName": "psi_desktop",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "number",
              "readOnly": false,
              "removed": true
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.dataTable",
      "typeVersion": 1.1,
      "position": [
        2640,
        0
      ],
      "id": "4ea05789-be59-ed46-0f1d-f59709f416dc",
      "name": "Zapisz do tabeli"
    }
  ],
  "connections": {
    "Uruchom recznie": {
      "main": [
        [
          {
            "node": "Ustawienia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ustawienia": {
      "main": [
        [
          {
            "node": "Przygotuj tabele",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Przygotuj tabele": {
      "main": [
        [
          {
            "node": "Pobierz sitemap",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pobierz sitemap": {
      "main": [
        [
          {
            "node": "XML na JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "XML na JSON": {
      "main": [
        [
          {
            "node": "Czy to indeks map?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Czy to indeks map?": {
      "main": [
        [
          {
            "node": "Lista podmap",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Adresy z mapy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lista podmap": {
      "main": [
        [
          {
            "node": "Rozbij podmapy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rozbij podmapy": {
      "main": [
        [
          {
            "node": "Pobierz podmape",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pobierz podmape": {
      "main": [
        [
          {
            "node": "XML podmapy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "XML podmapy": {
      "main": [
        [
          {
            "node": "Adresy z podmapy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Adresy z podmapy": {
      "main": [
        [
          {
            "node": "Rozbij adresy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Adresy z mapy": {
      "main": [
        [
          {
            "node": "Rozbij adresy",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Rozbij adresy": {
      "main": [
        [
          {
            "node": "Zapisz do tabeli",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}