Stara strona firmy HVAC spod Warszawy wyglądała bogato: wideo w tle, liczniki, logotypy partnerów. W Google nie miała ani jednej frazy w pierwszej setce wyników. Strona główna ważyła 6,2 MB, a na telefonie największy element ładował się 7,8 sekundy. Strona firmy myjącej okna była skromniejsza: jedna podstrona i galeria, w której zdjęcia wklejono do HTML-a jako tekst base64. Od 17 września do dnia audytu dała 3 kliknięcia z Google.
Obie strony przebudowałem i obie zacząłem od godziny z audytem. Audyt mówi mi, co naprawić, i daje właścicielowi powód, żeby w ogóle rozmawiać o nowej stronie.
Krok 1. Plan godziny
Godzina wystarcza, jeśli trzymam się kolejności i niczego nie poprawiam w trakcie. Audyt to zdjęcie stanu, naprawy przyjdą później.
| Minuty | Co robię | Czym |
|---|---|---|
| 0-10 | adresy, robots, sitemap, tytuł, opis, H1, zewnętrzne skrypty, formularz | skrypt audyt.py z kroku 2 |
| 10-25 | szybkość i waga strony na telefonie | Lighthouse albo PageSpeed Insights |
| 25-40 | strona w ręku: telefon, menu, formularz, podstrony usług | własny telefon, nie emulator |
| 40-50 | indeksacja i frazy | site:, Search Console, narzędzie do fraz |
| 50-60 | tabela „problem i skutek” plus werdykt w dwóch zdaniach | notatka w Markdown |
Wizytówkę Google i konkurencję robię w drugiej godzinie. To temat następnej lekcji.
Krok 2. Skrypt do pierwszych dziesięciu minut
Te rzeczy sprawdza się zawsze tak samo, więc zamknąłem je w skrypcie. Jest w samym Pythonie, bez dodatkowych bibliotek, działa od wersji 3.9.
# audyt.py - szybki audyt techniczny strony lokalnej firmy
# uzycie: python audyt.py domena.pl
import re
import sys
import urllib.request
import urllib.error
from urllib.parse import urlparse
sys.stdout.reconfigure(encoding="utf-8")
UA = {"User-Agent": "Mozilla/5.0 (audyt strony)"}
def pobierz(url):
"""Zwraca (status, adres koncowy, tresc) - podaza za przekierowaniami."""
try:
r = urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=20)
return r.status, r.geturl(), r.read()
except urllib.error.HTTPError as e:
return e.code, url, b""
except Exception as e:
return 0, url, str(e).encode()
domena = sys.argv[1].replace("https://", "").replace("http://", "").strip("/")
gola = domena.removeprefix("www.")
print("== Wersje adresu (powinny prowadzic do jednej) ==")
koncowe = set()
dzialajace = []
for wariant in (f"http://{gola}/", f"http://www.{gola}/", f"https://{gola}/", f"https://www.{gola}/"):
status, koniec, _ = pobierz(wariant)
koncowe.add(koniec)
if status == 200:
dzialajace.append(koniec)
print(f"{wariant:40} {status} -> {koniec}")
print("OK, jeden adres docelowy" if len(koncowe) == 1 else f"UWAGA: {len(koncowe)} rozne adresy docelowe")
if not dzialajace:
sys.exit("Zaden wariant adresu nie odpowiada 200")
baza = next((k for k in dzialajace if k.startswith("https")), dzialajace[0]).rstrip("/")
for plik in ("/robots.txt", "/sitemap.xml", "/index.html"):
status, _, tresc = pobierz(baza + plik)
print(f"{plik:14} {status} {len(tresc)} B")
print("\n== Strona glowna ==")
status, url, surowe = pobierz(baza + "/")
html = surowe.decode("utf-8", errors="replace")
print(f"waga samego HTML: {len(surowe) / 1024:.0f} KB")
def jedno(wzor):
m = re.search(wzor, html, re.I | re.S)
return m.group(1).strip() if m else ""
tytul = jedno(r"<title[^>]*>(.*?)</title>")
opis = jedno(r'<meta[^>]+name=["\']description["\'][^>]+content=["\']([^"\']*)')
print(f"title ({len(tytul)} zn.): {tytul}")
print(f"description ({len(opis)} zn.): {opis[:120]}")
kanon = [t for t in re.findall(r"<link[^>]+>", html, re.I) if "canonical" in t.lower()]
print("canonical:", kanon[0] if kanon else "BRAK")
print("viewport:", "jest" if re.search(r'name=["\']viewport', html, re.I) else "BRAK")
h1 = [re.sub(r"<[^>]+>", "", h).strip() for h in re.findall(r"<h1[^>]*>(.*?)</h1>", html, re.I | re.S)]
print(f"H1 ({len(h1)}):", h1[:3])
print("telefon jako link tel:", "jest" if "tel:" in html else "BRAK")
print("dane strukturalne JSON-LD:", len(re.findall(r"application/ld\+json", html)))
b64 = re.findall(r"data:image/[a-z]+;base64,([A-Za-z0-9+/=]+)", html)
if b64:
print(f"obrazy wklejone w HTML (base64): {len(b64)} szt., {sum(map(len, b64)) / 1024:.0f} KB")
hosty = {}
for zasob in re.findall(r'(?:src|href)=["\'](https?://[^"\']+)', html):
h = urlparse(zasob).netloc
if gola not in h:
hosty[h] = hosty.get(h, 0) + 1
print("zewnetrzne hosty:", dict(sorted(hosty.items(), key=lambda x: -x[1])) or "brak")
for akcja in re.findall(r'<form[^>]+action=["\']([^"\']*)', html, re.I):
print("formularz wysyla do:", akcja or "(ta sama strona)")
print("\n== Linki wewnetrzne (podstrony) ==")
linki = set()
for h in re.findall(r'href=["\']([^"\'#?]+)', html):
if gola in h or not re.match(r"(https?:|mailto:|tel:|javascript:|//)", h):
sciezka = urlparse(h).path or "/"
if not re.search(r"\.(css|js|png|jpe?g|webp|svg|ico|woff2?)$", sciezka):
linki.add(sciezka)
print(len(linki), "unikalnych adresow:", sorted(linki)[:15])
Uruchamiam go poleceniem python audyt.py nazwa-domeny.pl. Co pokazał na starych stronach:
- Firma HVAC: http, https, z www i bez www odpowiadały kodem 200, a
/index.htmlbył kopią strony głównej. Razem pięć wersji tej samej strony. Robots i sitemap zwracały 404. - Tam też tytuł 88 znaków i opis 266 znaków (Google obetnie oba), a H1 „Ponad 25 lat” z nazwą firmy, czyli bez frazy, o którą ktoś pyta.
- Formularz wysyłał dane do zewnętrznej usługi z USA, z wyłączoną captchą, bez zgody i bez polityki prywatności.
- Siedem logotypów partnerów ładowało się z usługi, która przestała działać. Siedem błędów przy każdym wejściu.
- Firma myjąca okna: 133 KB samego HTML-a na stronie głównej, z czego 113 KB to dwa zdjęcia w base64.
Krok 3. Szybkość na telefonie
Mierzę na profilu mobilnym, bo „mycia okien” i „serwisu klimatyzacji” szuka się z telefonu. Zapisuję ocenę wydajności, LCP (kiedy pojawia się największy element) i wagę strony. Właściciel rozumie „7,8 sekundy” i „6,2 MB” lepiej niż „63 na 100”.
Przy firmie HVAC od razu było widać, skąd wynik 63/100:
- wideo w tle 23 MB z autoodtwarzaniem i bez plakatu, czyli obrazka pokazywanego przed startem filmu,
- zdjęcie zmiękczacza wody w JPG 3,4 MB i logo w PNG 472 KB,
- biblioteka ikon w wersji beta z zewnętrznego CDN i krój Inter w siedmiu grubościach, które blokowały renderowanie. Lighthouse wycenił to na około 3 sekundy.
Lighthouse odpalam z terminala, żeby wynik został w pliku obok późniejszego wyniku nowej strony. Potrzebny jest Node.js i Chrome, a telefon jest emulowany domyślnie:
npx lighthouse https://adres-firmy.pl --only-categories=performance,accessibility,seo --output=html --output-path=./lh-stara.html
Krok 4. Strona w ręku
Ten kwadrans robię na własnym telefonie, nie w trybie mobilnym przeglądarki. Klikam to, co kliknąłby klient:
- Czy numer telefonu da się kliknąć i czy wybiera właściwy numer.
- Czy menu się otwiera i zamyka.
- Czy jest cena, choćby „od”. Kto jej nie znajdzie, dzwoni do konkurencji, która ją podaje.
- Czy formularz cokolwiek wysyła i dokąd. Wysyłam testową wiadomość i proszę właściciela, żeby sprawdził skrzynkę.
- Czy każda podstrona usługi ładuje się w całości. U firmy HVAC tło pięciu podstron usług miało ścieżkę względną
images/unnamed (2).webp. Z adresu/uslugi/x/przeglądarka szukała obrazka w złym katalogu i dostawała 404. - Czy zdjęcia są prawdziwe. Na tej samej stronie jeden awatar wygenerowany przez AI udawał zdjęcie czterech różnych doradców.
Przy innych redesignach wyszły rzeczy, których automat nie złapie. Londyńska firma kamieniarska miała na stronie „O nas” treść demonstracyjną z kupionego motywu, ze zmyślonymi nagrodami i opiniami fikcyjnych klientów. W stopce wisiał koszyk sklepu z kwotą 0.00, choć firma nic nie sprzedaje online. Właściciel zwykle o tym nie wie, więc takie znaleziska zapisuję osobno.
Krok 5. Indeksacja i frazy
Zaczynam od site:domena.pl w Google. Liczba wyników jest przybliżona, ale pokazuje, czy Google zna 1 adres, czy 40. Jeśli dostanę dostęp do Search Console, biorę liczby stamtąd. U firmy myjącej okna była jedna strona zaindeksowana i 3 kliknięcia.
Frazy sprawdzam w narzędziu, które pokazuje, na co domena jest widoczna. Używam DataForSEO (płatne za zapytanie), ten sam widok mają Ahrefs, Semrush i Senuto. U firmy HVAC wyszło 0 fraz w top 100, a hurtownia z tej samej grupy miała 34 frazy. Ten kontrast trafił do pierwszego zdania raportu.
Przy kancelarii adwokackiej wystarczyło spojrzeć w kod. Stara strona była jedną stroną z kotwicami #zespol i #specjalizacje, więc Google indeksował jeden adres. Sitemap wskazywała na inną domenę.
Firma myjąca okna pokazała drugą stronę medalu. Organicznie nie było jej nigdzie, ale w mapce nad wynikami stała na pierwszym miejscu w trzech dzielnicach. Audyt samej strony powiedziałby, że firma jest niewidoczna, a to nieprawda. Dlatego zawsze robię go w parze z audytem wizytówki.
Krok 6. Z audytu argument
Ostatnie dziesięć minut to dokument dla właściciela: werdykt w dwóch zdaniach i tabela „problem i skutek”, najwyżej dziesięć wierszy. Werdykt dla firmy HVAC brzmiał mniej więcej tak: strona wygląda bogato, ale w Google nie ma ani jednej frazy w top 100, a lokalny popyt na klimatyzację i pompy ciepła w ich rejonie jest do wzięcia.
| Problem | Skutek |
|---|---|
| Brak robots.txt i sitemap.xml | Google nie dostaje listy adresów |
| Cztery wersje adresu bez przekierowań | pięć kopii tej samej strony głównej |
| Wideo 23 MB bez plakatu | LCP 7,8 s, strona 6,2 MB |
| Formularz przez zewnętrzną usługę, bez zgody | spam i dane klientów u obcej firmy z USA |
| Ten sam awatar AI jako czterech doradców | wygląda nieprofesjonalnie |
Protip: kolumnę „skutek” piszę językiem właściciela. „Brak przekierowań 301” nic mu nie mówi, „pięć kopii tej samej strony głównej” już tak. Liczbę techniczną zostawiam w nawiasie dla kogoś, kto będzie to sprawdzał.
Pod tabelą dopisuję listę „do weryfikacji”. U firmy HVAC była na niej ocena 4,9 przy 127 opiniach, wpisana ręcznie w dane strukturalne strony. Z tej listy robię pytania na rozmowę z właścicielem.
Zadanie: audyt jednej strony w godzinę
Wybierz lokalną firmę usługową ze swojego miasta, najlepiej taką, której strona wyraźnie męczy się na telefonie. Zrób audyt według planu z kroku 1 i zmieść się w 60 minutach.
- Uruchomiłem
audyt.pyi zapisałem wynik. - Zmierzyłem stronę Lighthouse'em na profilu mobilnym: wydajność, LCP, waga.
- Przeklikałem stronę na własnym telefonie: numer, menu, cena, formularz, podstrony usług, zdjęcia.
- Sprawdziłem
site:i widoczność fraz w dowolnym narzędziu SEO. - Mam werdykt w dwóch zdaniach i tabelę „problem i skutek” do dziesięciu wierszy.
- Mam listę „do weryfikacji” z pytaniami do właściciela.