Serwis obyty.pl miał być o zdrowiu: 741 opublikowanych wpisów o objawach, badaniach i lekach. Kiedy we wrześniu otworzyłem Search Console, wyświetlenia zbierały zupełnie inne teksty. Porównanie Emirates A380 z Boeingiem 777 miało ich 219, tekst o przeciętnym wynagrodzeniu według GUS 221, a poradnik o trzech formularzach do Wietnamu 67 wyświetleń i 5 kliknięć. Cały serwis zebrał w 90 dni 25 kliknięć.
To stare wpisy z poprzedniego wcielenia domeny. Audyt robiony według podręcznika kazałby je usunąć jako niepasujące do tematu, a to były jedyne strony, które w ogóle działały. Dlatego audyt dużego serwisu zaczynam od rozmowy i od pytania o pieniądze. Crawler odpalam później.
Krok 1. Pięć pytań na pierwszą rozmowę
Rozmowę prowadzę według stałej listy. Odpowiedzi zapisuję dosłownie, bo wracam do nich, kiedy układam priorytety.
- Z czego serwis żyje i które sekcje na to pracują. Reklama, afiliacja, subskrypcje, zapytania ofertowe. Na moim serwisie naukowym noktus.pl przychód 3 013 zł brał się z odsłon na wp.pl w programie WP Kreator, a nie z ruchu na własnej domenie. Licznik pokazywał wtedy 36 wejść dziennie przy 1 395 wpisach. Audyt indeksacji by tego nie wykrył, a po końcu programu pieniądze znikały bez względu na stan indeksu.
- Co się zmieniło i kiedy. Migracje, nowe szablony, wtyczki, automaty, które publikują albo czyszczą treść. Proszę o daty, choćby przybliżone.
- Kiedy spadł ruch i według którego narzędzia. Analityka, Search Console i panel reklamowy liczą różne rzeczy.
- Czego nie wolno ruszać.
- Kto wdroży zalecenia i ile ma na to czasu. Lista osiemdziesięciu zadań dla zespołu z jednym programistą na pół etatu zostanie w szufladzie, choćby każde zadanie było słuszne.
Drugie pytanie wygląda na formalność, dopóki nie trafisz na zmianę, o której nikt nie pamięta. Na promptowy.com czytelnik zgłosił, że strona wygląda jak goły HTML: niebieskie linki, punktory zamiast menu. Winna była wtyczka, która przy każdej publikacji czyściła cache razem ze zminifikowanymi plikami CSS i JS. Przy 7-24 wpisach dziennie arkusze znikały kilkanaście razy na dobę, a strony w tym oknie odwoływały się do pliku, którego nie było. Bez pytania o automaty szukałbym błędu w szablonie.
Krok 2. Sekcje i koncentracja ruchu
Odpowiedzi klienta sprawdzam w danych, zanim cokolwiek zacznę crawlować. Skrypt pobiera z Search Console wszystkie strony z wyświetleniami i sumuje je według pierwszego fragmentu ścieżki, czyli sekcji. Drugi wynik to koncentracja: jaki procent kliknięć robi najmocniejsza strona, pierwsza piątka, dwudziestka i setka.
# sekcje.py - ktore sekcje serwisu robia ruch z Google (Search Console API)
# uzycie: python sekcje.py https://www.domena.pl/ 28
# wymaga: pip install google-auth requests oraz klucz.json konta uslugi
import csv
import sys
import datetime as dt
from collections import defaultdict
from urllib.parse import quote, urlparse
from google.oauth2 import service_account
from google.auth.transport.requests import AuthorizedSession
sys.stdout.reconfigure(encoding="utf-8")
WLASCIWOSC = sys.argv[1]
DNI = int(sys.argv[2]) if len(sys.argv) > 2 else 28
klucz = service_account.Credentials.from_service_account_file(
"klucz.json", scopes=["https://www.googleapis.com/auth/webmasters.readonly"])
sesja = AuthorizedSession(klucz)
API = ("https://searchconsole.googleapis.com/webmasters/v3/sites/"
+ quote(WLASCIWOSC, safe="") + "/searchAnalytics/query")
koniec = dt.date.today() - dt.timedelta(days=3) # dane maja 2-3 dni opoznienia
start = koniec - dt.timedelta(days=DNI - 1)
def wszystkie_strony():
"""Stronicuje po 25 000 wierszy. API sortuje po kliknieciach i ucina reszte."""
wiersze, od = [], 0
while True:
r = sesja.post(API, json={
"startDate": start.isoformat(), "endDate": koniec.isoformat(),
"dimensions": ["page"], "rowLimit": 25000, "startRow": od}, timeout=120)
r.raise_for_status() # blad to wyjatek, nie pusta lista
paczka = r.json().get("rows", [])
wiersze += paczka
if len(paczka) < 25000:
return wiersze
od += 25000
dane = wszystkie_strony()
if not dane:
sys.exit("Zero wierszy z API. Nie licze udzialow - sprawdz wlasciwosc i daty.")
sekcje = defaultdict(lambda: [0, 0, 0]) # klikniecia, wyswietlenia, strony
for w in dane:
sciezka = urlparse(w["keys"][0]).path.strip("/")
nazwa = "/" + sciezka.split("/")[0] + "/" if sciezka else "/ (glowna)"
s = sekcje[nazwa]
s[0] += w["clicks"]
s[1] += w["impressions"]
s[2] += 1
suma = sum(w["clicks"] for w in dane) or 1
print(f"{start} - {koniec}: {len(dane)} stron z wyswietleniami, {suma:.0f} klikniec")
ranking = sorted(sekcje.items(), key=lambda x: -x[1][0])
for nazwa, (k, wy, n) in ranking[:25]:
print(f"{nazwa[:45]:45} {k:>9.0f} klik {k / suma:6.1%} {wy:>11.0f} wysw {n:>7} stron")
po_klik = sorted(dane, key=lambda w: -w["clicks"])
for n in (1, 5, 20, 100):
udzial = sum(w["clicks"] for w in po_klik[:n]) / suma
print(f"pierwsze {n:>3} stron = {udzial:.0%} klikniec")
with open("sekcje.csv", "w", newline="", encoding="utf-8") as f:
zapis = csv.writer(f)
zapis.writerow(["sekcja", "klikniecia", "wyswietlenia", "strony"])
for nazwa, (k, wy, n) in ranking:
zapis.writerow([nazwa, k, wy, n])
Uruchamiam go poleceniem python sekcje.py https://www.domena.pl/ 28. Potrzebne są biblioteki google-auth i requests oraz plik klucza konta usługi, o którym piszę w następnej lekcji. Na WordPressie z wpisami w katalogu głównym pierwszy fragment ścieżki to sam wpis. Wtedy sekcje biorę z eksportu kategorii, a ze skryptu zostaje mi koncentracja.
Stronicowanie po 25 000 wierszy jest tam celowo. API sortuje wiersze po kliknięciach i ucina je na limicie. Na promptowy.com zapytałem kiedyś o 500 stron i wyszło mi, że tylko 3 z 41 recenzji mają jakiekolwiek wyświetlenia. Serwis miał 3 269 stron z wyświetleniami, a recenzje z zerem kliknięć po prostu nie zmieściły się w limicie. Po powtórzeniu pomiaru wyświetlenia miało 38 z 40.
Koncentracja na promptowym wyglądała tak: trzy strony o filtrze lat 80. dawały 57 procent kliknięć z 28 dni, pierwsza piątka 72 procent. Awaria szablonu tych trzech adresów kosztowałaby więcej niż bałagan w pozostałych ponad 2 500 adresach z map witryny. Od tej proporcji zależy kolejność całego audytu.
Krok 3. Liczby klienta i liczby z zewnątrz
Przy audycie black-box dużego serwisu o grach nie miałem żadnych danych od wydawcy. Miałem jego materiały reklamowe w dwóch wersjach, starszej i z grudnia 2025 roku. Liczba użytkowników spadła między nimi mniej więcej o połowę. W raporcie podałem to jako tło dla priorytetów i dopisałem wprost, że to dane wydawcy, których nie sprawdziłem.
Pytanie biznesowe brzmiało więc: gdzie serwis traci. Odpowiedź dało zestawienie jego zasobów z wynikami wyszukiwania. Serwis trzymał się tam, gdzie miał coś własnego: bazę około 24 800 gier z ocenami graczy i około 31 500 poradników. Na zapytaniach sezonowych, jak kalendarz premier czy wymagania sprzętowe, wyżej stały sklepy i serwisy specjalistyczne. To była obserwacja wyników, a nie pomiar w narzędziu, i tak ją opisałem.
Z tego wyszła oś raportu. Porządek w indeksie był potrzebny po to, żeby robot i linki wewnętrzne szły do bazy gier i poradników, a nie do pustych stron. Bez pytania „gdzie tracimy” ten sam zestaw błędów trafiłby do raportu jako płaska lista.
Krok 4. Z pytań robię zakres
Każde pytanie zamieniam na to, co sprawdzę, i na źródło danych.
| Pytanie | Co sprawdzam | Skąd dane |
|---|---|---|
| Które sekcje zarabiają | udział sekcji w kliknięciach, stan indeksu tych sekcji | Search Console, analityka |
| Co się zmieniło | szablony, wtyczki i automaty zmienione przed spadkiem | lista wdrożeń od klienta, repozytorium kodu |
| Kiedy spadł ruch | wykres dzienny zestawiony z datami zmian | Search Console, analityka |
| Czego nie ruszać | wyłączenia w rekomendacjach | rozmowa, umowy z partnerami |
| Kto wdroży | wielkość i forma zadań w raporcie | rozmowa z zespołem technicznym |
Z tej tabeli wychodzą hipotezy, a nie wnioski. „Spadek po migracji w marcu” sprawdzam w danych jak każdą inną tezę. Jeśli dane jej nie potwierdzają, mówię to klientowi, także wtedy, gdy sam ją przyniósł.
Protip: zakres zapisuję jako pytania, na które raport odpowie, a nie jako obszary techniczne. Zdanie „czy archiwum newsów jest w indeksie i czy robot do niego wraca” klient rozumie i po oddaniu raportu może je odhaczyć. „Analiza indeksacji” nie mówi mu nic i rozrasta się bez końca.
Krok 5. Lista „nie ruszam”
Ostatni element zakresu to strony i sekcje, których audyt nie dotknie albo dotknie tylko z opisem ryzyka. Na obyty.pl trafiło na tę listę około 46 wpisów z kategorii podróżniczych: tanie loty, noclegi, transport, bagaż. W serwisie o zdrowiu wyglądały na balast, a ruch robiły właśnie one i kilka tekstów o gospodarce.
Drugą część listy wypełnia klient: sekcje sponsorowane, umowy z partnerami, strony pilnowane przez zarząd. Jeśli w trakcie audytu wyjdzie, że któraś z nich szkodzi serwisowi, opisuję to osobno, z ryzykiem i kosztem, ale bez polecenia „usunąć”. Decyzja należy wtedy do klienta, a ja mam ją na piśmie.
Zadanie: karta zakresu dla jednego serwisu
Weź serwis z co najmniej kilkoma tysiącami adresów, własny albo klienta, do którego masz dostęp w Search Console. Przygotuj kartę zakresu, zanim uruchomisz jakikolwiek crawler.
- Odpowiedziałem na pięć pytań z kroku 1 i zapisałem odpowiedzi dosłownie.
- Uruchomiłem
sekcje.pydla 28 dni i mam pliksekcje.csv. - Wiem, jaki procent kliknięć robi pierwsza piątka stron.
- Dla trzech najmocniejszych stron rozbiłem kliknięcia na zapytania i odsiałem zapytania nawigacyjne.
- Mam tabelę „pytanie - co sprawdzam - skąd dane”.
- Mam listę „nie ruszam” z podpisem klienta albo z jego mailem.