Darmowy crawler SEO w Arkuszach Google – poradnik z formułami IMPORTXML i Apps Script

Spis treści

Masz pytanie o swoją stronę?

Umów krótką konsultację – powiem, od czego zacząć.

Prosty, darmowy crawler SEO zbudujesz w Arkuszach Google z dwóch elementów: funkcji IMPORTXML, która za pomocą zapytań XPath pobiera ze strony title, meta opis, nagłówki, linki czy treść, oraz krótkiego skryptu Apps Script, który sprawdza kod odpowiedzi HTTP i przekierowania – tego IMPORTXML nie potrafi. Takie rozwiązanie sprawdzi się przy niewielkich stronach renderowanych po stronie serwera (np. WordPress); przy dużych serwisach i stronach generowanych przez JavaScript lepszy będzie Screaming Frog, Sitebulb albo własny skrypt.

Od razu mówię – jeżeli jesteś właścicielem dużego e-commerce lub serwisu treściowego, to co tutaj znajdziesz może być niewystarczające. Dla tych z Was, którzy posiadają jednak nieco mniejsze strony, w tym strony firmowe, serwisy treściowe lub nieduże sklepy internetowe, chcę pokazać, jak możecie wykonać sobie prosty crawler. Poświęcając odpowiednią ilość czasu, stworzysz coś na kształt Sitebulba lub Screaming Froga i to zupełnie za darmo.

Miej jednak na uwadze, że płatne narzędzia nie są płatne bez przyczyny. One po prostu działają niezależnie od tego, czy strona jest wielka, za hasłem czy napisana w Angularze. Crawler, który Ci zaproponuję, nie będzie niestety mógł zbadać stron dynamicznych generowanych przez frameworki JavaScript. Aby móc sensownie wykorzystać arkusz, musisz posiadać chociaż minimalną wiedzę o budowie stron i o tym, jak pozyskać pewne informacje z kodu źródłowego witryny.

Arkusze Google

Jeśli kiedykolwiek musiałeś tworzyć tabelki, liczyć coś w arkuszu kalkulacyjnym albo dzielić się danymi z innymi, prawdopodobnie słyszałeś o Arkuszach Google. To darmowa aplikacja od Google, która pozwala na tworzenie, edytowanie i udostępnianie arkuszy kalkulacyjnych – i to wszystko bez potrzeby instalowania czegokolwiek na swoim komputerze. Wystarczy przeglądarka internetowa i dostęp do sieci!

Największym atutem Arkuszy Google jest to, że działają w chmurze. Co to oznacza w praktyce? Możesz otworzyć swoje pliki na każdym urządzeniu – na komputerze, tablecie, a nawet na telefonie – gdziekolwiek jesteś. Nie musisz nosić ze sobą pendrive’a czy wysyłać plików e-mailem. Wystarczy, że masz dostęp do internetu, a wszystkie Twoje arkusze są zawsze pod ręką.

Kolejna sprawa, która naprawdę wyróżnia Arkusze Google, to możliwość współpracy w czasie rzeczywistym. Wyobraź sobie, że tworzysz raport z kolegą z pracy. W przypadku Excela musiałbyś zapisywać, wysyłać e-mailem, czekać na odpowiedź, potem ściągać i edytować. W Arkuszach Google? Możecie pracować razem, w jednym momencie, w tym samym pliku. Ty wpisujesz dane, a on dodaje komentarze. Wszystko odbywa się płynnie, a zmiany są widoczne od razu. I co najlepsze – nawet jak zapomnisz kliknąć „zapisz”, Arkusze Google robią to za Ciebie automatycznie!

Jak zacząć? Prościej być nie może! Jeśli masz konto Google (np. Gmail), to właściwie masz już dostęp do Arkuszy Google. Wystarczy, że wejdziesz na stronę sheets.google.com i gotowe – możesz zacząć tworzyć swoje pierwsze arkusze.

Co będzie nam potrzebne?

Zacznę od tego, że nie jestem jakimś wymiataczem Excela/Arkuszy. Można wręcz powiedzieć, że jestem bardziej średnio niż zaawansowany. Ale mam po swojej stronie chęć realizacji celu, wiedzę oraz ChatGPT. Jeżeli ja czegoś nie będę wiedział i nie będę umiał zrobić, ChatGPT z radością zaproponuje mi odpowiednie rozwiązanie. Mało tego – pokażę Ci, jak swój pomysł przekuć w odpowiednie zapytanie i otrzymać dzięki temu odpowiednią formułę albo skrypt. I spokojnie, jeżeli JA sobie z czymś poradziłem, to Ty poradzisz sobie z tym bez żadnego problemu. Nie zniechęcaj się!

Po pierwsze, musisz posiadać konto Google z dostępem do Dysku Google. Nie ma znaczenia, czy będziesz korzystał z płatnego konta Google, czy też nie. Możliwe, że płatne konto będzie działać bardziej wydajnie i możliwe będzie otrzymanie większej porcji danych w szybszym tempie. Takie miałem obserwacje, gdy jeszcze korzystałem z Google Workspace (płatne konto Google). Potwierdzona różnica dotyczy limitów Apps Script: konto prywatne może wykonać 20 000 zapytań UrlFetch dziennie, a konto Google Workspace 100 000.

Po drugie polecam stworzyć darmowe (a jakże) konto w ChatGPT lub innym asystencie AI. Pozwoli Ci to na rozmowę z AI i otrzymanie dopasowanego do Twoich potrzeb zestawu formuł. Spokojnie, nie będziesz musiał wszystkiego robić samodzielnie od zera.

Trzecia kwestia to posiadanie strony internetowej, którą chcemy zbadać. Idealnie sprawdzą się witryny zbudowane na WordPressie. Niezależnie od silnika strony ważne – dla naszej wygody – jest, aby posiadać mapę strony w wersji XML, np. pod adresem https://semurai.pl/sitemap_index.xml. W przypadku stron zbudowanych na WordPressie wystarczy zainteresować się wtyczkami takimi jak Yoast SEO, Rank Math czy AIOSEO.

Teraz idziemy do kuchni, robimy sobie kawę, wracamy do komputera i jedziemy z tematem.

Tworzenie SEO crawlera w Arkuszach Google

Pora zabrać się za tworzenie naszego narzędzia. Pozwól, że wyjaśnię Ci nieco, czym w ogóle jest crawler SEO oraz czego możesz oczekiwać po swoim arkuszu po wykonaniu wszystkich zaleceń. Zanim jednak do tego przejdziemy, kilka ważnych informacji i uwag.

Czym jest crawler SEO?

Crawler SEO to narzędzie, które przeszukuje strony internetowe w podobny sposób, jak robią to wyszukiwarki (np. Google), aby zbierać informacje o strukturze, zawartości i błędach na stronie. Takie programy pomagają analizować serwisy pod kątem optymalizacji dla wyszukiwarek (SEO). Crawler odwiedza każdą stronę witryny, śledzi linki, analizuje zawartość, zbiera dane o tytułach, opisach, obrazkach, szybkości ładowania i wielu innych elementach, które mają wpływ na widoczność strony w wynikach wyszukiwania.

Uczciwie trzeba zaznaczyć, że arkusz, który zbudujemy, nie „chodzi” samodzielnie po linkach jak prawdziwy crawler. To raczej scraper listy adresów: Ty podajesz listę URL-i (np. z mapy strony), a arkusz pobiera dla każdego z nich wybrane dane. Przy małej stronie efekt jest bardzo podobny.

Co robią aplikacje takie jak Sitebulb czy Screaming Frog?

Narzędzia takie jak Sitebulb i Screaming Frog to popularne crawlery SEO, które pomagają w audytach stron internetowych. Dzięki nim możesz:

  • Sprawdzić, czy strona ma problemy z linkami (np. linki, które prowadzą donikąd – tzw. błędy 404).
  • Zidentyfikować braki w optymalizacji, np. zbyt długie lub krótkie tytuły i metaopisy, brak nagłówków H1, niezoptymalizowane obrazy.
  • Przeanalizować strukturę linków wewnętrznych, co jest kluczowe dla odpowiedniego indeksowania przez wyszukiwarki.
  • Znaleźć problemy z szybkością ładowania strony lub plikami, które utrudniają jej dostępność.
  • Sprawdzić, czy strona posiada poprawnie wdrożone tagi kanoniczne i metadane SEO.

Te narzędzia oferują pełną kontrolę nad audytem SEO, dzięki czemu możesz zobaczyć, jak wyszukiwarki „widzą” Twoją stronę i co możesz poprawić, by zwiększyć widoczność w Google.

Czy Google Search Console jest niewystarczające?

Google Search Console (GSC) to potężne narzędzie, ale ma swoje ograniczenia. GSC dostarcza danych o tym, jak Google indeksuje Twoją stronę, jakie ma błędy i jak strona radzi sobie w wynikach wyszukiwania (np. ile kliknięć i wyświetleń mają poszczególne podstrony). Jest świetne do monitorowania stanu witryny i sprawdzania kluczowych wskaźników, ale nie zastępuje crawlera SEO.

Główne różnice są takie, że GSC:

  • Pokazuje dane o adresach, które Google zna i przetworzyło – nie o każdej podstronie, jaką masz w serwisie.
  • Nie pozwala na pełne, szczegółowe audyty techniczne stron, np. nie wyświetla pełnych list brakujących metadanych czy problemów z linkowaniem wewnętrznym, które crawlery takie jak Screaming Frog czy Sitebulb mogą wykryć.

Podsumowując, Google Search Console daje cenne dane od samego Google, ale crawlery SEO są niezbędne, gdy chcesz mieć pełną kontrolę nad optymalizacją techniczną i szybciej wychwycić problemy SEO, zanim wpłyną na pozycję Twojej strony w wynikach wyszukiwania. Dlatego też posiadanie crawlera SEO jest naprawdę przydatne. A najlepiej jeszcze – jeżeli jest on w zasadzie za darmo!

Co chcemy osiągnąć?

Główną motywacją dla mnie do stworzenia całego konceptu crawlera SEO była chęć szybkiego znalezienia miejsc, gdzie mogę dodać sensowne linki wewnętrzne między wpisami, oraz uzyskania kompletu informacji na temat widoczności każdego z wpisów/adresów. Zakładam, że każdy wpis na stronie widoczny będzie na wiele różnych słów kluczowych. Do tego jeszcze różne narzędzia mogą dostarczać różne dane. Połączenie tych danych i informacji sprawia, że mogę otrzymać najbardziej kompleksowy obraz sytuacji.

Albo mogę mieć narzędzie do ściągania treści ze stron w celu dalszej ich obróbki. Możliwości jest w zasadzie nieskończenie wiele. Wszystko zależy tak naprawdę od tego, na czym nam najbardziej zależy.

Przyjmijmy na potrzeby tego poradnika, że chcemy stworzyć arkusz, który powie nam:

  • Jak wyglądają nasze meta znaczniki
  • Jak długie są nasze meta znaczniki
  • Jaką treść mamy w ogóle na stronie
  • Jak długa jest interesująca nas właściwa treść na stronie
  • Jakie nagłówki h1/h2/h3 wykorzystujemy w naszym tekście
  • Ile obrazków znajduje się w treści
  • Na jakie słowa kluczowe prezentuje się wskazany URL
  • Jaka jest szacowana liczba wyszukań, jaką może wygenerować dany artykuł

I pewnie jeszcze wiele więcej można wyciągnąć. Skupmy się jednak na tych kwestiach. Będą dobrą bazą do dalszych działań. Dwa ostatnie punkty to dane z zewnętrznych narzędzi (GSC, Senuto, Ahrefs itp.) – wrócę do nich na końcu.

Tworzenie crawlera SEO

Dość gadania, czas zabrać się za konkrety! Zacznij od otworzenia pustego arkusza Google. Jeżeli jesteś zalogowany do swojego konta Google, wpisz w pasek adresu przeglądarki sheets.new (działa też sheet.new). To od razu przeniesie Cię do nowego, pustego arkusza. Możesz też po prostu odwiedzić swój Dysk Google i utworzyć nowy plik.

Nowy, pusty arkusz Google przygotowany pod crawler SEO

Jakie informacje możemy pobrać o naszej stronie do Arkusza Google?

Stwórzmy na początek strukturę naszego dokumentu. Pozwoli nam to łatwiej komunikować się tu między sobą, ale także z ChatemGPT. Pamiętaj też, że zawsze możesz dodać własne rozwiązania. Mało tego – mam nadzieję, że rozwiniesz ten arkusz dokładnie pod swoje potrzeby i możliwości! Ja chcę pokazać Ci tylko możliwości, jakie masz przy tworzeniu własnego crawlera SEO w Arkuszu Google!

Oto elementy, które będziemy chcieli dodać do naszego dokumentu:

  • URL
  • Kod odpowiedzi (status HTTP)
  • Title
  • Długość title
  • Meta opis
  • Długość meta opisu
  • Tytuł wpisu (H1)
  • Treść strony
  • Długość treści strony
  • Liczba nagłówków h2
  • Liczba nagłówków h3
  • Liczba pozostałych nagłówków

Możesz oczywiście dodać więcej rzeczy! Te jednak będą przydatne zwłaszcza na początek. Przejdźmy do omawiania kolejnych elementów, które umieścimy w naszym arkuszu.

Zanim zaczniesz, pamiętaj o tym

Pewnie szukając informacji w sieci, a nawet korzystając z ChatGPT, otrzymasz formuły, które z jakiegoś powodu nie będą działać. Niby wszystko będzie wyglądało poprawnie, ale wciąż otrzymywać będziesz komunikat o błędnej formule. To znak, że pewnie użyty został zły znak do rozdzielenia argumentów formuły. O co chodzi? Separator argumentów zależy od ustawień regionalnych arkusza (Plik → Ustawienia → Ustawienia regionalne). Przy ustawieniach anglojęzycznych (np. Stany Zjednoczone) separatorem jest przecinek „,”. Przy ustawieniach regionalnych Polska przecinek jest separatorem dziesiętnym, dlatego argumenty oddziela się średnikiem „;”. Wszystkie formuły w tym poradniku są zapisane w wersji dla polskich ustawień regionalnych, czyli ze średnikiem. Przykładowo:

To jest poprawna formuła, która zadziała w Twoim arkuszu Google z polskimi ustawieniami regionalnymi. Zwróć uwagę, że po „A2” znajduje się znak „;”.

=IMPORTXML(A2;"/*")

To jest także poprawna formuła, ale dla arkusza z anglojęzycznymi ustawieniami regionalnymi. W arkuszu ustawionym na Polskę otrzymasz błąd składni, bo przecinek po „A2” nie jest tu interpretowany jako separator argumentów.

=IMPORTXML(A2,"/*")

Uwaga na pułapkę: zamieniaj na średnik tylko przecinki między argumentami funkcji. Przecinki wewnątrz zapytania XPath, np. w contains(@class,'entry-content'), są częścią składni XPath i muszą zostać przecinkami.

Dużo energii straciłem na dojście do tego 🙂 Podobnie jest z nazwami funkcji: przy polskim języku interfejsu Arkusze mogą wyświetlać nazwy funkcji po polsku (np. DŁ zamiast LEN). Steruje tym opcja „Zawsze używaj angielskich nazw funkcji” w ustawieniach arkusza. W tym poradniku używam nazw angielskich, a polskie odpowiedniki podaję tam, gdzie to ważne.

Druga sprawa: Arkusze Google mają limity! Nie nastawiaj się, że uda Ci się przejrzeć stronę z wieloma tysiącami podstron. Przejrzenie strony z kilkudziesięcioma podstronami będzie natomiast raczej bez problemu możliwe. Konkretne ograniczenia zebrałem poniżej.

Limity i ograniczenia crawlera w Arkuszach Google

OgraniczenieCo to oznacza w praktyce
Odświeżanie IMPORTXML / IMPORTHTML / IMPORTDATAWedług dokumentacji Google te funkcje aktualizują dane co 1 godzinę. Po zmianie na stronie wynik w arkuszu nie zmieni się od razu.
Brak kodu odpowiedzi w IMPORTXMLIMPORTXML zwraca tylko dane z dokumentu. Statusu HTTP (200, 301, 404) ani przekierowań nim nie sprawdzisz – do tego potrzebny jest Apps Script.
Czas działania funkcji niestandardowej (Apps Script)Maksymalnie 30 sekund na wykonanie. Wolna strona = błąd #ERROR! w komórce.
Dzienny limit zapytań UrlFetch20 000 dziennie na koncie prywatnym, 100 000 na koncie Google Workspace.
Pamięć podręczna (CacheService)Maksymalnie 6 godzin (21 600 s), klucz do 250 znaków, wartość do 100 KB. Google zaznacza, że dane mogą zniknąć z cache wcześniej.
Strony renderowane w JavaScriptIMPORTXML i UrlFetchApp pobierają surowy HTML z serwera, bez wykonywania JavaScriptu. Treści doładowane skryptem nie zobaczysz.
Blokady po stronie serweraZapytania wychodzą z infrastruktury Google, a nie z Twojej przeglądarki. Część zabezpieczeń (np. Cloudflare, firewalle hostingu) potrafi je odrzucać – wtedy zamiast danych dostaniesz błąd pobierania adresu URL.

Ostatni punkt to nie jest udokumentowana reguła, tylko często spotykana sytuacja: jeżeli ta sama formuła działa na jednej stronie, a na innej uparcie zwraca błąd pobierania, w pierwszej kolejności podejrzewaj ochronę antybotową serwera.

Przy każdym elemencie poniżej znajdziesz liczbę gwiazdek. To moje określenie tego, jak trudne jest pobranie danego elementu. Im więcej gwiazdek, tym trudniejsze może być odpowiednie przygotowanie danych lub konieczne będzie wykorzystanie dodatkowych elementów, takich jak Apps Script czy analiza kodu HTML.

URL (★★☆☆☆)

Możliwości są tutaj dwie. Albo będziemy wklejać sobie wybrane przez nas pojedyncze adresy, co jest bardzo proste i działa na 99%, albo możemy skorzystać z mapy strony w formacie XML. Stosując jednak mapę strony w formacie XML, mogą zdarzyć się sytuacje, że natrafimy na problemy z jej pobieraniem. Konstrukcja mapy może się delikatnie różnić w zależności od stosowanego CMS-a czy nawet wtyczki. Tak, już na tym etapie zaczyna się zabawa z „to zależy”.

Ważna rzecz: plik sitemap_index.xml to indeks map strony – zawiera adresy kolejnych map (np. post-sitemap.xml, page-sitemap.xml), a nie adresy podstron. Do arkusza wskazuj więc konkretną mapę z wpisami lub stronami.

Dlatego najprostsze, co możesz zrobić, to skopiować adresy z mapy strony lub wkleić URL-e, które chcesz sprawdzić, i mieć święty spokój z SEO-wą ruletką – pobierze się albo się nie pobierze.

Jakie zapytania mogą pobrać adresy z mapy strony? Najpewniejsze jest to, które ignoruje przestrzeń nazw XML (mapy stron mają zadeklarowany namespace sitemaps.org, przez co prosty selektor potrafi nie zwrócić nic):

=IMPORTXML(A2;"//*[local-name()='loc']")

Krótsza wersja, którą możesz spróbować w pierwszej kolejności:

=IMPORTXML(A2;"//loc")

Jeżeli zamiast mapy XML wskażesz zwykłą stronę HTML (np. stronę z listą wpisów albo mapę strony w HTML), adresy linków pobierzesz atrybutem @href:

=IMPORTXML(A2;"//a/@href")

Uwaga: częsty błąd to zapis //a/href (bez @) – taka formuła nic nie zwróci, bo href jest atrybutem, a nie elementem. Z kolei tabelka, którą widzisz po otwarciu mapy XML w przeglądarce, to tylko wizualizacja nakładana przez wtyczkę (arkusz stylów XSL). IMPORTXML pobiera surowy XML, więc selektory w rodzaju //td/a zwykle nie zadziałają na mapie XML.

Jak sprawdzić czy Arkusz poprawnie łączy się z moją mapą strony?

Jeżeli podejmujesz wyzwanie pobrania danych z mapy strony i wydaje Ci się, że powinno działać, ale jednak coś nie do końca gra, sprawdź takie rozwiązanie. Wprowadź tę formułę do dowolnej komórki:

=IMPORTXML(A2;"/*")

Powinieneś otrzymać zrzut z całego pliku podobny do tego:

Wynik formuły IMPORTXML z zapytaniem /* – zawartość mapy strony XML wczytana do Arkusza Google

To jest jasna informacja pt. połączenie z Twoim plikiem zostało poprawnie ustawione, ale coś jest nie tak z przekazanymi selektorami. Czasem najłatwiej jest ręcznie wkleić interesujące nas adresy – będzie to oszczędność czasu i nerwów.

Nagłówek odpowiedzi (★★★★☆)

Chcesz sprawdzić, czy adres, z którym się łączysz, w ogóle działa? A może ma ustawione przekierowanie? Możesz otrzymać informacje w różnej formie i z różnym stopniem skomplikowania. Tym razem skupię się jednak na dostarczeniu Ci prostej informacji w postaci kodu odpowiedzi. IMPORTXML tego nie potrafi, więc musimy wykorzystać coś, co nazywa się Apps Script, czyli napisać skrypt w JavaScripcie, który będzie wykonywał pewne działanie.

Aby wykorzystać Apps Script, musisz w swoim arkuszu wybrać menu Rozszerzenia, a następnie Apps Script.

Menu Rozszerzenia w Arkuszach Google z opcją Apps Script

Otworzy się nowa karta z edytorem. Nie będę teraz tłumaczył Ci, jak z tego korzystać w sposób zaawansowany. Po prostu weź kod poniżej i wklej go do edytora.

Edytor Apps Script z wklejoną funkcją getStatusCode sprawdzającą kod odpowiedzi HTTP

Oto kod, który musisz wkleić do edytora (zaktualizowana wersja – obsługuje puste komórki, błędy połączenia i różną wielkość liter w nagłówku Location):

function getStatusCode(url) {
  if (!url) return "";
  const urlTrimmed = String(url).trim();
  const cache = CacheService.getScriptCache();
  // klucz cache może mieć maks. 250 znaków
  const key = urlTrimmed.length <= 240 ? "s_" + urlTrimmed : null;
  let result = key ? cache.get(key) : null;

  if (!result) {
    try {
      const response = UrlFetchApp.fetch(urlTrimmed, {
        muteHttpExceptions: true, // nie przerywaj przy 404, 500 itd.
        followRedirects: false    // pokaż 301/302 zamiast kodu strony docelowej
      });
      result = String(response.getResponseCode());
      if (key) cache.put(key, result, 21600); // 6 godzin – maksimum dla CacheService
    } catch (e) {
      return "Błąd: " + e.message; // np. nieistniejąca domena, timeout
    }
  }
  return Number(result);
}

function getRedirects(url) {
  if (!url) return "";
  const urlTrimmed = String(url).trim();
  const cache = CacheService.getScriptCache();
  const key = urlTrimmed.length <= 240 ? "r_" + urlTrimmed : null;
  let result = key ? cache.get(key) : null;

  if (!result) {
    try {
      const response = UrlFetchApp.fetch(urlTrimmed, {
        followRedirects: false,
        muteHttpExceptions: true
      });
      const headers = response.getHeaders();
      const location = headers["Location"] || headers["location"];
      result = location ? String(location) : "brak przekierowania";
      if (key) cache.put(key, result, 21600);
    } catch (e) {
      return "Błąd: " + e.message;
    }
  }
  return result;
}

Mając już ten kod wklejony, naciśnij w edytorze CTRL+S lub kliknij ikonkę zapisu (dyskietki). Nie musisz klikać Uruchom. Zamiast tego wróć do arkusza i w kolumnie, w której chcesz, aby pojawiały się odpowiedzi, wpisz:

=getStatusCode(ADRES_KOMÓRKI_Z_URL)
Formuła =getStatusCode(D3) w Arkuszu Google zwracająca kod odpowiedzi 200

Naciśnij Enter i poczekaj chwilę. Skrypt wyświetli kod odpowiedzi wskazanego URL-a. Możesz śmiało wykonać ten skrypt dla wielu adresów! Wystarczy skopiować formułę do nowych komórek i wskazać kolejne adresy. Jest to najłatwiejszy sposób na pobranie statusu naszej strony.

W tym samym skrypcie jest druga funkcja – getRedirects. Jeżeli adres zwraca 301 lub 302, pokaże Ci, dokąd przekierowuje (wartość nagłówka Location):

=getRedirects(ADRES_KOMÓRKI_Z_URL)

Pamiętaj, że to tylko pierwszy krok przekierowania, a nie koniecznie adres końcowy. Jeśli dostaniesz kolejny adres z przekierowaniem, to masz łańcuch przekierowań – warto go skrócić. Serwer może też zwrócić adres względny (np. /nowy-adres/).

Dwie rzeczy, o których warto wiedzieć. Po pierwsze, wynik jest zapamiętywany w cache do 6 godzin, a funkcja niestandardowa przelicza się ponownie tylko wtedy, gdy zmieni się wartość komórki, do której się odwołuje – po poprawieniu przekierowania na stronie wynik w arkuszu może się nie zmienić od razu. Po drugie, każda komórka to osobne zapytanie do serwera, więc przy setkach adresów łatwo trafić na limit czasu lub limity serwera (kod 429). Jeżeli masz do sprawdzenia tysiące adresów, lepiej sprawdzi się masowa analiza kodów odpowiedzi skryptem w Pythonie.

Dla osób, które nie wiedzą, do czego może się to przydać i czym są statusy nagłówka:

Statusy nagłówka strony (statusy HTTP) to kody odpowiedzi zwracane przez serwer w odpowiedzi na żądanie przeglądarki internetowej. Informują one o powodzeniu lub problemie z realizacją żądania. Poniżej znajdują się najpopularniejsze kody odpowiedzi:

  1. 200 OK – Żądanie zostało przetworzone poprawnie, a serwer zwraca oczekiwane zasoby (np. stronę HTML).
  2. 301 Moved Permanently – Zasób został trwale przeniesiony pod inny adres URL. Przeglądarka automatycznie przechodzi pod nowy adres.
  3. 302 Found – Tymczasowe przekierowanie na inny adres URL. Zasób może być dostępny w przyszłości pod starym adresem.
  4. 404 Not Found – Serwer nie znalazł zasobu pod podanym adresem URL. Strona nie istnieje lub została usunięta.
  5. 410 Gone – Zasób został trwale usunięty i nie będzie już dostępny pod tym adresem.
  6. 503 Service Unavailable – Serwer jest tymczasowo niedostępny (np. z powodu przeciążenia lub konserwacji). Zwykle problem jest chwilowy.
  7. 429 Too Many Requests – Serwer odrzucił żądanie, ponieważ wysłano zbyt wiele zapytań w krótkim czasie (przekroczono limity zapytań).

Te kody pomagają przeglądarkom, robotom wyszukiwarek i użytkownikom zrozumieć, co dzieje się z danym żądaniem oraz jak na nie reagować.

Title (★☆☆☆☆)

Pobieranie tekstu ze znacznika <title> jest banalnie proste. Praktycznie każda strona posiada znacznik title, więc coś tam musi się pobrać. Pobranie title to mała formuła:

=IMPORTXML(ADRES_KOMÓRKI;"//title//text()")

Dodanie text() w formule, takiej jak =IMPORTXML(D3;"//title//text()"), służy do wyodrębnienia samego tekstu z określonego elementu HTML, w tym przypadku z elementu <title>.

Oto, co robi każdy składnik tej formuły:

  • IMPORTXML(D3; "//title"): pobiera cały element <title> ze strony, której URL znajduje się w komórce D3.
  • //text(): wyodrębnia tylko treść tekstową wewnątrz tego elementu, pomijając znaczniki HTML.

Przykład: Jeśli strona ma kod HTML:

<title>Przykładowy tytuł strony</title>

Formuła =IMPORTXML(D3;"//title//text()") zwróci tylko „Przykładowy tytuł strony”, bez tagów <title>.

Formuła IMPORTXML z zapytaniem //title//text() pobierająca title strony do arkusza

Uwaga: niektóre motywy umieszczają w kodzie strony więcej niż jeden znacznik <title> (np. wewnątrz grafik SVG). Jeżeli formuła zwraca kilka wierszy, ogranicz zapytanie do sekcji head: =IMPORTXML(ADRES_KOMÓRKI;"//head/title").

Meta opis (★☆☆☆☆)

Pobranie meta opisu jest także dosyć proste. Tutaj jednak częściej może zdarzyć się sytuacja, że meta opis będzie nieuzupełniony lub wręcz nie dodany w ogóle. Ostatnio częściej trafiałem na treści umieszczone w znaczniku og:description niż w meta description.

og:description to meta tag używany w protokole Open Graph, który jest stosowany głównie przez media społecznościowe (np. Facebook, LinkedIn) do wyświetlania podglądu treści, gdy strona jest udostępniana. Ten tag definiuje krótki opis strony, który pojawia się w takich podglądach.

Z kolei meta description to standardowy meta tag HTML, który zapewnia krótki opis strony dla wyszukiwarek (np. Google) i może być wyświetlany w wynikach wyszukiwania jako fragment pod linkiem do strony.

Różnica:

  • og:description jest przeznaczony dla platform społecznościowych i stosowany w udostępnianiu linków.
  • meta description jest używany przez wyszukiwarki – nie jest czynnikiem rankingowym, ale wpływa na to, jak Twój wynik może wyglądać w Google, a więc i na klikalność.

Oba tagi mogą zawierać różne opisy, aby dopasować treść do różnych kontekstów.

Mimo wszystko pobranie tekstu z meta opisu także nie należy do najtrudniejszych! Wystarczy jedna, prosta formuła z prostym selektorem.

=IMPORTXML(ADRES_KOMÓRKI; "//meta[@name='description']/@content")
Formuła IMPORTXML pobierająca meta description strony do Arkusza Google

Jeżeli chciałbyś pobrać treść ze znacznika og:description (lub dowolnego innego znacznika), musisz skorzystać z formuły:

=IMPORTXML(ADRES_KOMÓRKI; "//meta[@property='og:description']/@content")

Może się to przydać np. przy sprawdzaniu, jak strona prezentuje się w mediach społecznościowych. Bo pobrać możesz niemalże dowolny znacznik czy treść ze swojej strony! Trzeba tylko wiedzieć, co chce się pobrać. Dwa przykłady, które bardzo przydają się w audycie – adres kanoniczny i meta robots:

=IMPORTXML(ADRES_KOMÓRKI; "//link[@rel='canonical']/@href")
=IMPORTXML(ADRES_KOMÓRKI; "//meta[@name='robots']/@content")

Jeżeli znacznika nie ma, IMPORTXML zwróci błąd #N/A (zaimportowana treść jest pusta). Żeby w takich miejscach widzieć czytelną informację, owiń formułę w IFERROR, np. =IFERROR(IMPORTXML(ADRES_KOMÓRKI; "//meta[@name='description']/@content"); "BRAK").

Tytuł wpisu/podstron i pozostałe nagłówki (★★☆☆☆)

Pobieranie tytułu wpisu czy też podstrony może być albo banalnie proste, albo nieco wymagające. Jeżeli Twoja strona zbudowana jest poprawnie z założeniami SEO, to tytuł każdej podstrony czy wpisu będzie umieszczony w znaczniku <h1>, w którym znajdzie się nazwa podstrony (np. O nas) lub nazwa wpisu. Wtedy tak naprawdę wystarczy jedna bardzo prosta formuła, której nie trzeba nawet tłumaczyć:

=IMPORTXML(ADRES_KOMÓRKI; "//h1")

To wszystko. Tytuł Twojego wpisu już ładuje się do arkusza. Jeżeli Twoja podstrona posiada kilka znaczników <h1> lub innych nagłówków i chcesz je wszystkie pobrać i wyświetlić w jednej komórce, np. po przecinku, zastosuj taką formułę:

=TEXTJOIN(", "; TRUE; IMPORTXML(ADRES_KOMÓRKI; "//h1"))

TEXTJOIN pomija puste komórki i radzi sobie także wtedy, gdy IMPORTXML rozłoży nagłówek na kilka kolumn (zwykły JOIN przyjmuje tylko jeden wiersz lub kolumnę). Możesz śmiało zmienić przecinek na inny znak. Tak samo //h1 możesz zmienić na przykład na //h2, //h3 itd. Lepiej nie dopisuj tu /text() – jeśli nagłówek zawiera w środku inne znaczniki (np. <strong> albo <span>), //h1/text() zwróci tylko fragmenty tekstu albo podzieli nagłówek na kawałki.

A jak policzyć nagłówki? Wystarczy policzyć wiersze, które zwraca IMPORTXML:

=IFERROR(ROWS(IMPORTXML(ADRES_KOMÓRKI; "//h2")); 0)

Zamień //h2 na //h3 albo na //h4 | //h5 | //h6, żeby policzyć pozostałe nagłówki. Tą samą metodą policzysz obrazki w treści wpisu, np. =IFERROR(ROWS(IMPORTXML(ADRES_KOMÓRKI; "//div[contains(@class,'entry-content')]//img/@src")); 0) – liczymy atrybut src, bo sam element <img> nie ma treści tekstowej. Jeżeli strona używa leniwego ładowania obrazków, adres bywa przeniesiony do atrybutu data-src – wtedy zamień @src na @data-src. Dlaczego ROWS, a nie COUNTA? Bo gdy na stronie nie ma żadnego nagłówka, IMPORTXML zwraca błąd #N/A, a COUNTA policzyłaby ten błąd jako 1. Kombinacja IFERROR + ROWS poprawnie pokaże 0.

A jeżeli okaże się, że Twoja strona nie posiada tytułów/nazw podstron w znaczniku h1, konieczne będzie wyszukanie odpowiedniej ścieżki selektora i pobranie treści z odpowiedniej części kodu strony. Potraktuj to jako rozwiązanie tymczasowe, bo przede wszystkim musisz zadbać o wstawienie znaczników <h1> tam, gdzie ich miejsce! Niemniej więcej o tym, jak wyszukać odpowiedni fragment kodu, w następnym punkcie.

Treść strony (★★★★☆)

Najtrudniejsza część tak naprawdę. Tutaj jest bardzo dużo zmiennych, które mogą się wydarzyć i powodować problemy. Pierwszy problem to próba połączenia się z witryną, która serwuje treść poprzez JavaScript. IMPORTXML pobiera wyłącznie HTML zwrócony przez serwer i nie wykonuje skryptów, więc treści doładowanej dynamicznie nie zapiszesz w ten sposób do arkusza Google*.

Druga sprawa to pobieranie tylko tego fragmentu treści, który nas faktycznie interesuje. Pobranie całej zawartości między znacznikiem <body> a </body> sprawi, że będziemy mieć wiele „śmieci” w postaci głównego menu, sidebarów czy stopki strony. Oczywiście, jeżeli są nam te dane potrzebne – śmiało! Niemniej, aby pobrać tylko część interesującej nas treści, będziemy musieli trochę pogrzebać w kodzie i niestety – nie ma tu drogi na skróty. Pocieszeniem niech będzie, że jeżeli raz poczujesz i zrozumiesz, o co chodzi, to kolejny raz będzie już dużo prostszy.

Dodatkowo w 99% przypadków dla jednej strony wystarczy przygotować jeden selektor i będzie on wykorzystywany na wszystkich podstronach danego typu! Czyli teoretycznie przekazujesz selektor raz i możesz pobierać treści z wielu podstron.

*można próbować obejść to przez zewnętrzną usługę (proxy) renderującą stronę, którą wywołuje arkusz, ale to już zupełnie inny poziom zabawy.

Jak zabrać się do wyszukania selektora?

Po pierwsze i najważniejsze, wchodzimy na stronę, z której treść chcemy pobrać. Po wejściu na nią otwieramy Narzędzia dla deweloperów (najczęściej CTRL+SHIFT+I lub F12, albo odpowiednia opcja z menu przeglądarki).

Narzędzia dla deweloperów w przeglądarce otwarte na wpisie blogowym – podgląd kodu HTML

Możesz też zaznaczyć interesujący Cię fragment strony i z menu kontekstowego dostępnego po kliknięciu prawym przyciskiem myszy wybrać opcję „Zbadaj”:

Menu kontekstowe przeglądarki z opcją Zbadaj na fragmencie treści wpisu

Którąkolwiek z opcji wybierzesz, dopiero teraz zacznie się cała zabawa z wyszukiwaniem odpowiedniego miejsca w kodzie strony. Chodzi ogólnie o to, aby znaleźć obszar (div), który będzie podświetlał interesujący Cię fragment strony, ale z pominięciem wszelkich niepotrzebnych elementów. Im mniej zewnętrznych divów weźmiesz pod uwagę, tym dokładniejsze dane dostaniesz!

Powyżej zobaczyć możesz, jak w zależności od elementu, który wybrałem, podświetlały się elementy na stronie. Gdybym potrzebował pobrać całą treść razem z tytułem wpisu, potrzebowałbym pobrać treść ze znacznika <main>. Jeżeli potrzebuję tylko treść, obszar <div class=”entry-content”…> byłby tym, który by mnie interesował.

Jak pobrać treść w arkuszu Google jeżeli znam obszar w którym on występuje?

To bardzo proste, bo w zasadzie najtrudniejsze mamy za sobą. Teraz tylko kwestia odpowiedniego dopracowania naszego selektora w formule. Dla powyższego przykładu sprawdzi się formuła:

=JOIN(" "; IMPORTXML(ADRES_KOMÓRKI; "//div[contains(@class,'entry-content')]//text()"))

Jest ona tak zbudowana, aby wszystkie fragmenty tekstu z obszarów z klasą „entry-content” zostały połączone w jedną całość w jednej komórce. Bez JOIN każdy fragment tekstu wyląduje w osobnej komórce:

=IMPORTXML(ADRES_KOMÓRKI; "//div[contains(@class,'entry-content')]//text()")

W przypadku nowszych stron opartych o WordPress najczęściej obszarem odpowiedzialnym za wyświetlanie treści we wpisie będzie obszar z klasą „entry-content”. W przypadku stron tworzonych pod konkretne potrzeby klientów obszary te mogą się nazywać zupełnie inaczej!

Tak prezentują się pobrane dane w arkuszu Google:

Treść wpisów pobrana do Arkusza Google formułą JOIN i IMPORTXML z klasą entry-content

Tak pobrane dane idealnie sprawdzą się do przejrzenia jakości treści, sprawdzenia jej długości czy dalszej obróbki z dodatkowymi danymi! Pamiętaj tylko, że pojedyncza komórka ma limit długości tekstu, więc bardzo długie artykuły mogą zwrócić błąd – wtedy pobieraj np. tylko akapity (//div[contains(@class,'entry-content')]//p).

Jak pobrać kod HTML danej podstrony?

Arkusze Google pozwalają na naprawdę sporo, mimo swoich limitów. Nie wiem, czy wiesz, ale możesz sobie pobrać na przykład cały fragment kodu HTML interesującej Cię podstrony. Co to da? A no stosując odpowiednie formuły, możesz sobie np. wyszukać linki wewnętrzne, linki wychodzące, linki follow, nofollow, ale także obrazki, teksty alternatywne czy cokolwiek innego, co będzie Cię interesować, a co znajduje się w kodzie HTML!

Kod HTML treści wpisu pobrany do Arkusza Google funkcją POBIERZ_HTML_Z_SELEKTOREM

Zanim sięgniesz po skrypt: większość tych informacji wyciągniesz samym IMPORTXML, bez pobierania kodu HTML. Kilka przykładów:

=IMPORTXML(ADRES_KOMÓRKI; "//div[contains(@class,'entry-content')]//a/@href")
=IMPORTXML(ADRES_KOMÓRKI; "//a[contains(@rel,'nofollow')]/@href")
=IMPORTXML(ADRES_KOMÓRKI; "//div[contains(@class,'entry-content')]//img/@alt")

Pierwsza formuła zwróci wszystkie linki z treści wpisu, druga – linki z atrybutem nofollow, trzecia – teksty alternatywne obrazków (obrazki bez atrybutu alt po prostu się nie pojawią).

Jeżeli jednak potrzebujesz surowego kodu HTML, musisz skorzystać z Apps Script. Niewykluczone też, że niezbędne będzie zapytanie ChatGPT o pomoc, na przykład przy wstawianiu odpowiedniej klasy diva. Jak widzisz bowiem w tej stworzonej przeze mnie formule, mamy nieco inną składnię niż w dotychczasowych IMPORTXML – podajesz klasę w stylu CSS, z kropką:

=POBIERZ_HTML_Z_SELEKTOREM(ADRES_KOMÓRKI;".entry-content")

Formuła ta odwołuje się do skryptu Apps Script. Poniżej jego poprawiona wersja – poprzednia kończyła pobieranie na pierwszym napotkanym </div>, więc jeśli w treści był zagnieżdżony div (a w WordPressie jest ich zwykle sporo), wynik był ucięty. Teraz skrypt liczy otwierane i zamykane divy:

// Funkcja niestandardowa: zwraca kod HTML pierwszego diva o wskazanej klasie
// Użycie w arkuszu: =POBIERZ_HTML_Z_SELEKTOREM(A2;".entry-content")
function POBIERZ_HTML_Z_SELEKTOREM(url, selector) {
  if (!url || !selector) return "";
  try {
    const match = String(selector).trim().match(/^\.([\w-]+)$/);
    if (!match) return "Nieprawidłowy selektor – podaj klasę, np. .entry-content";
    const className = match[1];

    const response = UrlFetchApp.fetch(String(url).trim(), { muteHttpExceptions: true });
    if (response.getResponseCode() !== 200) {
      return "Kod odpowiedzi: " + response.getResponseCode();
    }
    const html = response.getContentText();

    // znajdź otwierający <div> z daną klasą
    const openRe = new RegExp("<div\\b[^>]*class=[\"'][^\"']*\\b" + className + "\\b[^\"']*[\"'][^>]*>", "i");
    const start = html.search(openRe);
    if (start === -1) return "Nie znaleziono elementu dla selektora: " + selector;

    // licz zagnieżdżone divy, aż do zamknięcia tego, od którego zaczęliśmy
    const tagRe = /<\/?div\b[^>]*>/gi;
    tagRe.lastIndex = start;
    let depth = 0;
    let tag;
    let end = html.length;
    while ((tag = tagRe.exec(html)) !== null) {
      depth += tag[0].charAt(1) === "/" ? -1 : 1;
      if (depth === 0) { end = tagRe.lastIndex; break; }
    }
    // komórka arkusza ma limit długości tekstu – przycinamy wynik
    return html.substring(start, end).substring(0, 50000);
  } catch (e) {
    return "Błąd: " + e.message;
  }
}

To nadal uproszczone rozwiązanie oparte na wyrażeniach regularnych, a nie pełny parser HTML: obsługuje tylko selektor klasy (z kropką), zwraca pierwszy pasujący div i może pomylić się na nietypowym kodzie. Do szybkiej analizy własnej strony w zupełności wystarczy.

Liczymy długość pobranej treści

Tyle się mówi, że title czy meta opis powinny mieścić się w konkretnych wartościach. Albo agencje obiecują, że treści będą takiej czy innej długości. Wszystko warto mierzyć. Nie ma nic prostszego z pomocą Arkuszy Google i tego, co do tej pory zrobiliśmy.

Formuła =DŁ() licząca liczbę znaków pobranej treści w Arkuszu Google

Wystarczy, że w kolumnie obok (czy dowolnym miejscu, które nas interesuje) dodamy formułę:

=LEN(ADRES_KOMÓRKI)

Jeżeli Twój arkusz wyświetla polskie nazwy funkcji (jak na zrzucie powyżej), ta sama formuła to =DŁ(ADRES_KOMÓRKI). I już! Mamy policzone znaki ze spacjami w naszej komórce. Pamiętaj, że Google nie podaje sztywnego limitu znaków dla title – w wynikach wyszukiwania skraca go w razie potrzeby, zwykle do szerokości ekranu urządzenia. Liczba znaków to więc wskazówka, a nie twarda granica. Jeżeli chcemy policzyć słowa, sprawa nieco się komplikuje, ale generalnie sprowadza się do zastosowania tej formuły:

=IF(TRIM(ADRES_KOMÓRKI)=""; 0; COUNTA(SPLIT(TRIM(ADRES_KOMÓRKI); " ")))

TRIM usuwa nadmiarowe spacje, a SPLIT dzieli tekst po spacji – COUNTA liczy powstałe fragmenty, czyli słowa. Nic nie stoi na przeszkodzie, aby stosować oba rozwiązania. Arkusz Google przyjmie wiele 😉

Na koniec

Rozmowa z ChatGPT o poprawieniu skryptu Apps Script do pobierania HTML z selektorem

Spokojnie, niemal wszystkie rzeczy, które tu znajdziesz, jesteś w stanie wygenerować sobie z pomocą ChatGPT, zadając odpowiednie pytania! Wszystkie skrypty Apps Script zostały wygenerowane przez ChatGPT. Ja nie jestem niestety jeszcze biegły w programowaniu tak bardzo, żeby samodzielnie napisać takie rozwiązania… ale pracuję nad tym! 🙂 Jedna rada: kod od AI zawsze testuj na kilku adresach, zanim puścisz go na całą listę.

Mam świadomość, że dla większych stron taki arkusz będzie totalnie bezużyteczny… Chociaż powiem szczerze, że widziałem już wiele stron, w których dzięki podobnym arkuszom udawało się względnie szybko sprawdzać, co i jak linkować wewnętrznie, albo analizować zasadność istnienia danej treści… jak?

Arkusz Google łączący treść wpisów z danymi o słowach kluczowych, liczbie wyszukań i średniej pozycji

Nic nie stoi na przeszkodzie, aby do arkusza importować dane z Google Search Console, Senuto, Semstorm czy Ahrefs! Na tej podstawie możliwe jest stworzenie rozbudowanej analizy, która dostarczy nie tylko klientowi, ale także nam – specjalistom SEO – wiele cennych informacji. Dodajmy do tego jeszcze możliwość korzystania z API, proxy i tym podobnych rozwiązań, i nagle okazuje się, że Arkusze Google mogą nam naprawdę potężnie ułatwić życie!

Pamiętaj tylko, że taki arkusz to narzędzie pomocnicze. Jeżeli wolisz, żeby ktoś przejrzał Twoją stronę kompleksowo – technicznie, pod kątem treści i linkowania – sprawdź, jak wygląda audyt SEO.

Najczęściej zadawane pytania

Czy IMPORTXML pokaże kod odpowiedzi strony (200, 301, 404)?

Nie. IMPORTXML pobiera tylko zawartość dokumentu i nie zwraca statusu HTTP. Gdy strona zwraca błąd, w komórce zobaczysz po prostu błąd formuły. Kod odpowiedzi i adres przekierowania sprawdzisz funkcjami Apps Script opisanymi wyżej (UrlFetchApp z opcjami followRedirects: false i muteHttpExceptions: true).

Dlaczego IMPORTXML zwraca #N/A albo błąd pobierania adresu URL?

Najczęstsze powody: zapytanie XPath nic nie znalazło (np. brak znacznika na stronie albo błąd w selektorze), treść jest generowana przez JavaScript, serwer lub zabezpieczenie antybotowe blokuje zapytania z serwerów Google albo w formule użyto złego separatora argumentów. Zacznij od testu =IMPORTXML(A2;"/*") – jeśli zwraca dane, problem leży w selektorze.

Przecinek czy średnik w formułach Arkuszy Google?

To zależy od ustawień regionalnych arkusza (Plik → Ustawienia). Przy ustawieniach „Polska” argumenty oddziela się średnikiem, przy anglojęzycznych – przecinkiem. Przecinki wewnątrz zapytań XPath zostają bez zmian.

Jak często odświeżają się dane w arkuszu?

Według dokumentacji Google funkcje IMPORTXML, IMPORTHTML i IMPORTDATA aktualizują dane co godzinę. Funkcje Apps Script z tego poradnika przeliczają się, gdy zmieni się komórka z adresem, a wynik dodatkowo trzymają w cache do 6 godzin.

Ile podstron sprawdzę w Arkuszach Google?

Bez problemu kilkadziesiąt. Twardym, udokumentowanym limitem jest m.in. 20 000 zapytań UrlFetch dziennie na koncie prywatnym (100 000 w Google Workspace) i 30 sekund na wykonanie funkcji niestandardowej, ale w praktyce wcześniej zaczną przeszkadzać czas ładowania arkusza i limity samego serwera. Przy dużych serwisach lepiej sięgnąć po Screaming Froga, Sitebulb albo skrypt w Pythonie.

Do czego przydadzą się IMPORTHTML i IMPORTDATA?

IMPORTHTML pobiera całą tabelę lub listę z dokumentu HTML – podajesz adres, typ („table” albo „list”) i numer tabeli/listy na stronie, np. =IMPORTHTML(A2;"table";1). IMPORTDATA wczytuje pliki CSV i TSV, np. eksport danych z innego narzędzia opublikowany pod adresem URL.

Sprawdź prosty arkusz w praktyce!

Jeżeli potrzebujesz gotowy, prosty arkusz, który możesz wykorzystać u siebie przy stronie – mam coś dla Ciebie! Odwiedź link poniżej, skopiuj arkusz do swojego Dysku Google (Plik → Utwórz kopię) i korzystaj z niego, jak chcesz. Skrypty Apps Script kopiują się razem z arkuszem, ale możesz podmienić je na poprawione wersje z tego wpisu.

Prosty arkusz do analizy treści znajdziesz tutaj: Analiza treści w Arkuszu Google by SEMURAI

Tomasz Stopka

Tomasz Stopka

Specjalista SEO od 2008 roku · autor książki „Content Marketing”

Pracowałem w agencjach, w portalu Onet i na własny rachunek jako SEMURAI. W Akademii dzielę się tym, co sprawdziło się w praktyce – konkretnie i bez ściemy.

Czytaj także