Strona wielojęzyczna i hreflang: struktura adresów, tłumaczenia i błędy, które psują SEO
Blog
Technologia

Strona wielojęzyczna i hreflang: struktura adresów, tłumaczenia i błędy, które psują SEO

Wersje językowe czy krajowe, podkatalogi czy domeny, hreflang w HTML-u i w mapie strony, canonical, lokalizacja zamiast samego tłumaczenia i skrypt, który wyłapuje błędy

DualFroz - VulCode CEODualFroz - VulCode CEO·8 września 2026·20 min czytania

Strona wielojęzyczna działa w Google dobrze, gdy spełnia trzy warunki: każda wersja językowa ma własny adres, znaczniki hreflang łączą wersje między sobą, a treść jest naprawdę napisana w danym języku. Typowe problemy, takie jak angielska wersja pokazywana użytkownikom z Polski albo wersja niemiecka, która nie trafia do indeksu, biorą się z braku jednego z tych warunków.

Poniżej przechodzimy przez decyzje w kolejności, w jakiej trzeba je podjąć: czy potrzebujesz wersji językowych, czy krajowych, jaką strukturę adresów wybrać, jak wdrożyć hreflang i jak pogodzić go z canonicalem, co poza tekstem trzeba zmienić przy lokalizacji i jak znaleźć błędy, których Search Console już nie raportuje. Strona, którą czytasz, działa w siedmiu językach, więc większość tych decyzji podejmowaliśmy także u siebie.

W skrócie
  • Najpierw ustal, czy celujesz w języki, czy w kraje. Większości firm wystarczą wersje językowe, na przykład pl, en i de, bez kodów krajów.
  • Najprostsza struktura to podkatalogi jednej domeny: /pl/, /en/, /de/. Google odradza rozróżnianie wersji parametrami w adresie i ciasteczkami.
  • Każda wersja wskazuje w hreflang samą siebie i wszystkie pozostałe, a każda z pozostałych wskazuje ją z powrotem. Bez linków zwrotnych Google może zignorować znaczniki.
  • Canonical każdej wersji wskazuje ją samą, nigdy wersję w innym języku.
  • Nie przekierowuj automatycznie według języka przeglądarki ani adresu IP. Googlebot zwykle łączy się z adresów w USA i bez preferencji językowej, więc zobaczyłby tylko jedną wersję.

Najpierw decyzja: wersje językowe czy wersje krajowe

To rozróżnienie decyduje o kodach hreflang, strukturze adresów i ilości pracy przy utrzymaniu. Wersja językowa jest dla każdego, kto czyta w danym języku, niezależnie od kraju: jedna wersja angielska dla klientów z Wielkiej Brytanii, Irlandii, Holandii i Stanów. Wersja krajowa ma inną treść dla każdego kraju, nawet w tym samym języku: inne ceny i walutę, inne warunki dostawy, inne dokumenty prawne, inny numer telefonu.

Większości firm, które z polskiej strony chcą zrobić wielojęzyczną, wystarczą wersje językowe. Wersje krajowe mają sens dopiero wtedy, gdy oferta naprawdę różni się między krajami. Strona, której wersje en-GB i en-US różnią się tylko pisownią "colour" i "color", jest dla Google dwiema bardzo podobnymi stronami w tym samym języku i podwaja pracę przy każdej zmianie treści.

Kody w hreflang mają ścisły format. Dokumentacja Google o wersjach zlokalizowanych wymaga kodu języka z normy ISO 639-1, opcjonalnie z kodem regionu z ISO 3166-1 Alpha 2 po myślniku. Sam kod kraju nie jest poprawny, bo pierwszy kod zawsze oznacza język: be to białoruski, a nie Belgia. Kody zarezerwowane, takie jak UK albo EU, Google ignoruje, więc Wielka Brytania to en-GB. Kody spoza tych norm, na przykład es-419 dla hiszpańskiego w Ameryce Łacińskiej, nie są obsługiwane.

SytuacjaKody hreflangUwagi
Jedna wersja angielska dla wszystkichenNajczęstszy i najprostszy przypadek
Osobne wersje dla Wielkiej Brytanii i USA z różnymi cenamien-GB, en-US i enen jako wersja dla pozostałych anglojęzycznych
Wersja niemiecka dla Niemiec, Austrii i SzwajcariideJedna wersja, jeśli oferta jest wszędzie taka sama
Różne ceny i dostawa w Niemczech i Austriide-DE, de-ATTreść musi się realnie różnić, inaczej Google może uznać wersje za duplikaty
Wersja polskaplKod pl-PL nic nie dodaje, jeśli nie ma innych polskich wersji

Struktura URL strony wielojęzycznej: domeny, subdomeny czy podkatalogi

Każda wersja językowa musi mieć własny adres. Google w przewodniku po witrynach wielojęzycznych zaleca osobne adresy dla każdej wersji zamiast przełączania języka ciasteczkiem albo ustawieniami przeglądarki, bo przy takim przełączaniu robot może nie znaleźć wszystkich wersji. Ten sam przewodnik opisuje cztery możliwe struktury:

StrukturaPrzykładZaletyWady
Domeny krajoweexample.de, example.frJasny sygnał, dla jakiego kraju jest stronaKoszt i formalności przy każdej domenie, osobna infrastruktura, jedna domena to jeden kraj
Subdomenyde.example.comŁatwe do uruchomienia, mogą działać na różnych serwerachZ samego adresu nie zawsze widać, dla kogo jest wersja
Podkatalogiexample.com/de/Najłatwiejsze do wdrożenia i utrzymania, jedna domenaWszystkie wersje na jednym serwerze, trudniej je rozdzielić
Parametry w adresieexample.com?lang=deBrakGoogle odradza ten sposób

Dla większości firm najlepszym wyborem są podkatalogi. Jedna domena oznacza jeden certyfikat, jedną konfigurację serwera i jedną mapę strony, a linki z zewnątrz trafiają do jednej witryny, zamiast rozkładać się na kilka domen. Ta strona działa w ten sposób: siedem wersji językowych w podkatalogach /pl/, /en/, /de/, /fr/, /it/, /es/ i /ru/.

Domeny krajowe mają sens, gdy w różnych krajach działają w praktyce osobne firmy: z własną ofertą, zespołem i obsługą klienta. Kosztem jest nie tylko rejestracja. Każda domena zaczyna budowanie widoczności od zera, a niektóre rejestry krajowe stawiają wymagania co do siedziby albo obecności w danym kraju.

Lokalizacja serwera jest według Google jednym z sygnałów, dla kogo jest strona, ale obok niej Google wymienia domenę krajową, hreflang, lokalne adresy i numery telefonów, walutę i język treści. Osobny serwer w Niemczech nie jest więc warunkiem dobrej wersji niemieckiej. W Search Console nie ma już też ustawienia kraju docelowego: Google wycofał raport kierowania międzynarodowego, uznając, że wybór kraju w panelu miał małą wartość, i dalej wspiera hreflang.

Osobna decyzja dotyczy tego, czy tłumaczyć same adresy. /de/angebot czyta się lepiej niż /de/oferta i zawiera słowo, którego niemiecki użytkownik szuka. Wymaga za to tabeli, która łączy odpowiedniki między językami, bo nie da się ich wyliczyć przez podmianę prefiksu. Na naszej stronie polskie adresy mają polskie segmenty, na przykład /pl/oferty zamiast /pl/offer, a odpowiedniki są zapisane w kodzie routingu.

Jak działa hreflang i czego nie robi

Znaczniki hreflang mówią Google, że kilka adresów to ta sama treść w różnych językach albo dla różnych regionów, i pozwalają pokazać użytkownikowi wersję pasującą do jego języka. Jeśli ktoś szuka po angielsku, a strona ma angielską wersję podstrony, Google może pokazać właśnie ją zamiast polskiej.

Hreflang nie służy do rozpoznawania języka. Google pisze wprost, że nie używa ani hreflang, ani atrybutu lang w HTML-u, żeby ustalić język strony, tylko własnych algorytmów działających na widocznej treści. Strona oznaczona hreflang="de", na której główna treść jest po polsku, nie stanie się przez to niemiecką.

Wersje językowe nie są duplikatami. Dokumentacja Google mówi, że zlokalizowane wersje strony są traktowane jak duplikaty tylko wtedy, gdy główna treść nie została przetłumaczona. Tłumaczenie nie konkuruje więc z oryginałem, ale wersja z przetłumaczonym samym menu i polskim tekstem już tak.

Atrybut lang jest dalej potrzebny, tylko z innego powodu. Czytniki ekranu wybierają na jego podstawie wymowę, a przeglądarki biorą go pod uwagę, gdy proponują tłumaczenie strony. Określenie języka strony jest wymaganiem WCAG 3.1.1 na najniższym poziomie A, więc <html lang="de"> w wersji niemieckiej to obowiązek dostępności, a nie SEO.

Wdrożenie hreflang: trzy metody i jedna zasada

Google przyjmuje hreflang na trzy sposoby: jako znaczniki <link> w nagłówku HTML, jako nagłówek HTTP Link i jako wpisy w mapie strony XML. Wszystkie trzy działają tak samo. Dokumentacja dodaje, że można użyć wszystkich naraz, ale nie daje to żadnej korzyści, a utrzymanie trzech wdrożeń jest trudniejsze. Wybierz jedną metodę.

Najczęściej używa się znaczników w HTML-u. Polska wersja podstrony oferty wygląda tak:

pl-oferta.html · html
<html lang="pl">
<head>
  <link rel="canonical" href="https://www.example.com/pl/oferta">
  <link rel="alternate" hreflang="pl" href="https://www.example.com/pl/oferta">
  <link rel="alternate" hreflang="en" href="https://www.example.com/en/offer">
  <link rel="alternate" hreflang="de" href="https://www.example.com/de/angebot">
  <link rel="alternate" hreflang="x-default" href="https://www.example.com/en/offer">
</head>

Wersje angielska i niemiecka mają w nagłówku dokładnie ten sam zestaw czterech znaczników hreflang, zmienia się tylko lang i canonical. Z tego przykładu widać zasady z dokumentacji Google:

  • Każda wersja wskazuje samą siebie i wszystkie pozostałe. Polska strona ma hreflang pl wskazujący ją samą.
  • Linki zwrotne. Jeśli strona A wskazuje stronę B, strona B musi wskazywać stronę A. Jeśli tak nie jest, Google może zignorować te znaczniki albo zinterpretować je źle.
  • Pełne adresy. Z protokołem i domeną, nie /en/offer.
  • x-default dla pozostałych. Wartość x-default wskazuje stronę dla użytkowników, których język nie pasuje do żadnej wersji. Może to być strona wyboru języka albo wersja główna. U nas x-default wskazuje wersję angielską.

Przy wielu wersjach i wielu podstronach wygodniejsza bywa mapa strony, bo znaczniki nie obciążają wtedy każdej strony HTML. Każdy adres dostaje w niej własny wpis z pełnym zestawem odpowiedników, łącznie z samym sobą:

sitemap.xml · xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <url>
    <loc>https://www.example.com/pl/oferta</loc>
    <xhtml:link rel="alternate" hreflang="pl" href="https://www.example.com/pl/oferta"/>
    <xhtml:link rel="alternate" hreflang="en" href="https://www.example.com/en/offer"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://www.example.com/en/offer"/>
  </url>
  <url>
    <loc>https://www.example.com/en/offer</loc>
    <xhtml:link rel="alternate" hreflang="pl" href="https://www.example.com/pl/oferta"/>
    <xhtml:link rel="alternate" hreflang="en" href="https://www.example.com/en/offer"/>
    <xhtml:link rel="alternate" hreflang="x-default" href="https://www.example.com/en/offer"/>
  </url>
</urlset>

Nagłówek HTTP przydaje się dla plików, które nie są stronami HTML, na przykład cennika w PDF w dwóch językach:

naglowek.txt
Link: <https://www.example.com/pl/cennik.pdf>; rel="alternate"; hreflang="pl", <https://www.example.com/en/pricing.pdf>; rel="alternate"; hreflang="en"

Bez względu na metodę znaczniki powinny powstawać automatycznie z jednego źródła: tabeli, która mówi, jakie odpowiedniki ma każda podstrona. Ręcznie dopisywany hreflang rozjeżdża się przy pierwszej nowej podstronie, a błąd w jednym miejscu psuje linki zwrotne w kilku. Gdy podstrona nie ma jeszcze tłumaczenia, generator po prostu nie dodaje znacznika dla brakującego języka.

hreflang a canonical: każda wersja wskazuje siebie

Canonical i hreflang odpowiadają na różne pytania. Canonical mówi, który adres jest główną wersją tej samej treści. Hreflang mówi, które adresy są tą samą treścią w innym języku. Dlatego canonical wersji angielskiej wskazuje wersję angielską, a nie polską.

Błąd odwrotny bierze się zwykle z szablonu, który buduje canonical z adresu polskiej wersji. Wersja angielska mówi wtedy Google, że jej główną wersją jest strona polska, czyli że sama jest duplikatem, który nie musi trafić do indeksu. Hreflang mówi coś przeciwnego, a sygnały się wykluczają.

Druga zasada: hreflang wskazuje wyłącznie adresy kanoniczne, które zwracają kod 200. Adres, który przekierowuje, zwraca 404, ma noindex albo canonical do innej strony, nie jest dobrym odpowiednikiem. Najczęściej dzieje się tak po zmianie adresów w jednej wersji językowej, gdy pozostałe wersje dalej wskazują stare adresy.

Wersje w tym samym języku dla różnych krajów wymagają ostrożności. Jeśli de-DE i de-AT różnią się tylko walutą, Google może uznać je za bardzo podobne strony. Przewodnik Google radzi przy podobnych treściach w tym samym języku wybrać wersję preferowaną i użyć canonical razem z hreflang. Prościej jest zrobić jedną wersję de albo sprawić, żeby wersje naprawdę się różniły: ceną, dostawą, danymi kontaktowymi, dokumentami.

Tłumaczenie a lokalizacja: co zmienia się poza tekstem

Tłumaczenie zamienia tekst na inny język. Lokalizacja dostosowuje stronę do odbiorcy w innym kraju, a tekst jest tylko jej częścią. Strona przetłumaczona bez lokalizacji wygląda poprawnie do pierwszego formularza, w którym niemiecki klient nie może wpisać kodu pocztowego, bo walidacja oczekuje formatu 00-000.

ElementSamo tłumaczenieLokalizacja
CenyKwota w złotych z przetłumaczonym opisemWaluta odbiorcy i informacja, czy cena zawiera podatek
Liczby i datyFormat polski: 12 345,67 i 4.09.2026Format odbiorcy: 12,345.67 i 04/09/2026 w Wielkiej Brytanii, 9/4/2026 w USA
FormularzePrzetłumaczone etykietyWalidacja kodu pocztowego, numeru telefonu i numeru podatkowego dla danego kraju
Dokumenty prawneBrak albo link do polskiego regulaminuRegulamin, polityka prywatności i baner zgód w języku wersji
Przykłady i odniesieniaPolskie realia przetłumaczone dosłowniePrzykłady zrozumiałe dla odbiorcy, bez lokalnych skrótów i instytucji
Maile i komunikatyStrona przetłumaczona, potwierdzenie zamówienia po polskuMaile transakcyjne i komunikaty błędów w języku, w którym klient złożył zamówienie
Tytuły i opisy metaPrzetłumaczone z polskichNapisane pod frazy, których odbiorca szuka w swoim języku

Formaty liczb, walut i dat nie powinny być wpisane w kod ręcznie. Przeglądarki i Node.js mają wbudowane formatowanie zależne od języka i regionu:

formaty.mjs · javascript
const amount = 12345.67;
const date = new Date('2026-09-04T12:00:00Z');

const versions = [
  ['pl-PL', 'PLN'],
  ['en-GB', 'GBP'],
  ['en-US', 'USD'],
  ['de-DE', 'EUR'],
];

for (const [locale, currency] of versions) {
  const money = new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount);
  const day = new Intl.DateTimeFormat(locale, { timeZone: 'UTC' }).format(date);
  console.log(`${locale}  ${money}  ${day}`);
}

Wynik w Node.js 22:

wynik.txt
pl-PL  12 345,67 zł  4.09.2026
en-GB  £12,345.67  04/09/2026
en-US  $12,345.67  9/4/2026
de-DE  12.345,67 €  4.9.2026

Ten sam dzień to 04/09/2026 w Wielkiej Brytanii i 9/4/2026 w USA, więc data wpisana na sztywno w jednym formacie dla wszystkich anglojęzycznych wersji wprowadzi część czytelników w błąd. Spacje w polskiej i niemieckiej kwocie to spacje nierozdzielające, więc kwota nie złamie się w połowie na końcu wiersza. Intl stosuje też polską konwencję, w której liczby czterocyfrowej nie rozdziela się spacją: ta sama funkcja zwróci 1234,56 zł, a nie 1 234,56 zł.

Najwięcej pracy wymagają słowa kluczowe. Fraza przetłumaczona dosłownie często nie jest tą, którą ludzie wpisują. Łatwo to zobaczyć w odwrotnym kierunku: zagraniczna firma, która przetłumaczy "web development" dosłownie na "rozwój stron", napisze poprawny tekst pod frazą, której polski klient raczej nie wpisze, bo szuka "tworzenia stron internetowych". Dla każdego języka trzeba więc sprawdzić frazy osobno, a tytuły pisać pod nie, w języku i alfabecie treści, czego wymaga też Google przy linkach tytułowych.

Tłumaczenie maszynowe: kiedy pomaga, a kiedy szkodzi

Tłumaczenie maszynowe jest dziś na tyle dobre, że nadaje się na pierwszą wersję tekstu. Problem nie leży w samym narzędziu, tylko w publikowaniu wyniku bez czytania. Google w zasadach dotyczących spamu jako przykład nadużycia skalowanego wymienia generowanie wielu stron z treści pobranych z innych źródeł, także przez automatyczne tłumaczenie, gdy niewiele wnoszą dla użytkownika. Przykład dotyczy treści pobranych z zewnątrz, a nie tłumaczenia własnej strony, ale kryterium jest to samo: czy strona coś daje czytelnikowi. Jakość tłumaczenia ocenia on, a nie narzędzie.

Rozsądny proces dla firmy, która nie ma tłumacza w zespole:

1
Słownik pojęć

lista nazw usług, produktów i terminów branżowych z ustalonym tłumaczeniem, żeby ta sama usługa nie miała trzech nazw.

2
Tłumaczenie robocze

maszynowe albo przez tłumacza, ale zawsze ze słownikiem.

3
Korekta przez osobę znającą język

obowiązkowo dla strony głównej, podstron ofertowych, formularzy i dokumentów prawnych.

4
Frazy i tytuły

tytuły i opisy meta napisane pod frazy wyszukiwane w danym języku, a nie przetłumaczone z polskich.

5
Publikacja z hreflang

znaczniki dopiero dla wersji, która jest gotowa w całości.

Najgorsza opcja to wersja przetłumaczona częściowo. Przewodnik Google zwraca uwagę, że przetłumaczenie samych elementów szablonu przy głównej treści w jednym języku daje słabe doświadczenie użytkownika, bo ta sama treść pojawia się w wynikach kilka razy. Lepiej opublikować wersję angielską z dziesięcioma przetłumaczonymi w całości podstronami niż z pięćdziesięcioma, z których czterdzieści ma angielskie menu i polski tekst.

Przełącznik języka i automatyczne przekierowania

Google w przewodniku radzi wprost: unikaj automatycznego przekierowywania użytkowników z jednej wersji językowej na inną. Powód jest techniczny. Według dokumentacji o stronach dostosowujących się do lokalizacji domyślne adresy IP Googlebota wyglądają na amerykańskie, a robot domyślnie nie wysyła nagłówka Accept-Language. Strona, która przekierowuje według kraju albo języka przeglądarki, pokazuje robotowi zawsze tę samą wersję, a pozostałe mogą nie zostać pobrane.

Przekierowanie szkodzi też ludziom. Polak mieszkający w Niemczech, który kliknął polski wynik w Google, chce przeczytać polską stronę, a nie zostać przeniesiony na niemiecką, bo tak wynika z jego adresu IP. Lepiej działa podpowiedź bez przekierowania: pasek u góry strony z informacją "Ta strona jest dostępna po polsku" i linkiem do odpowiednika, wyświetlany tylko wtedy, gdy język przeglądarki nie pasuje do wersji strony.

Sam przełącznik języka też ma kilka zasad:

  • Prowadzi do odpowiednika, a nie na stronę główną. Kto czyta ofertę po polsku i przełącza na angielski, powinien zobaczyć ofertę po angielsku. Gdy odpowiednika nie ma, przełącznik może prowadzić do strony głównej wersji, ale powinien to wyraźnie zaznaczyć.
  • To zwykłe linki <a href>. Google zaleca linki między wersjami, a przełącznik działający tylko przez JavaScript bez adresu w href nie jest dla robota linkiem.
  • Nazwy języków w ich własnym brzmieniu. "Deutsch", "Polski", "English", a nie flagi. Flaga oznacza kraj, a nie język: angielski nie ma jednej flagi, a w Szwajcarii mówi się czterema językami.

Osobno trzeba obsłużyć adres główny domeny, czyli / bez prefiksu języka. To dobre miejsce na stronę wyboru języka albo wersję domyślną oznaczoną jako x-default.

Typowe błędy hreflang i jak je wykryć

Search Console nie ma już raportu, który pokazywał błędy hreflang, więc sprawdzanie zostaje po Twojej stronie. Najczęstsze błędy wyglądają tak:

BłądSkutekJak to wychwycić
Brak linku zwrotnego z jednej z wersjiGoogle może zignorować znaczniki dla tej parySkrypt poniżej, sprawdzenie każdej pary wersji
Brak wskazania samej siebieNiepełny zestaw, wersja nie jest częścią grupyPorównanie listy hreflang z adresem strony
Kod en-UK albo UKCzęść z regionem jest ignorowanaLista kodów w szablonie, poprawnie en-GB
Sam kod kraju zamiast języka, na przykład at dla AustriiZnacznik jest błędny, bo pierwszy kod zawsze oznacza językKażdy kod zaczyna się od kodu języka, dla Austrii de-AT
hreflang wskazuje adres z przekierowaniem albo 404Odpowiednik nie jest stroną kanonicznąKod odpowiedzi każdego adresu z hreflang
Canonical wskazuje inną wersję językowąWersja może zostać potraktowana jak duplikat, sygnały są sprzeczne z hreflangCanonical każdej wersji wskazuje ją samą
Adresy względne w hreflangGoogle wymaga pełnych adresówKażdy href zaczyna się od https://
Wszystkie wersje wskazują stronę główną innych językówUżytkownik trafia na stronę główną zamiast odpowiednikaOdpowiedniki z tabeli tłumaczeń, nie stałe adresy

Poniższy skrypt w Pythonie sprawdza większość z tych punktów dla podanych adresów. Nie wymaga instalowania bibliotek, sprawdzaliśmy go na Pythonie 3.12. Dla każdej strony pobiera nagłówek, odczytuje canonical i hreflang, a potem pobiera każdy odpowiednik i sprawdza, czy zwraca 200 bez przekierowania i czy wskazuje stronę wyjściową z powrotem.

sprawdz_hreflang.py · python
#!/usr/bin/env python3
"""Sprawdza hreflang i canonical. Użycie: python3 sprawdz_hreflang.py URL [URL ...]"""
import re
import sys
import urllib.error
import urllib.request
from html.parser import HTMLParser

CODE = re.compile(r"^(x-default|[a-z]{2}(-[a-z]{4})?(-[a-z]{2})?)quot;)
RESERVED_REGIONS = {"uk", "eu", "un"}


class HeadLinks(HTMLParser):
    def __init__(self):
        super().__init__()
        self.alternates = {}
        self.canonical = None

    def handle_starttag(self, tag, attrs):
        if tag != "link":
            return
        attributes = dict(attrs)
        rel = (attributes.get("rel") or "").lower().split()
        href = attributes.get("href")
        if "alternate" in rel and attributes.get("hreflang") and href:
            self.alternates[attributes["hreflang"].lower()] = href
        if "canonical" in rel and href:
            self.canonical = href


def fetch(url, cache):
    if url not in cache:
        request = urllib.request.Request(url, headers={"User-Agent": "hreflang-check"})
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                parser = HeadLinks()
                parser.feed(response.read().decode("utf-8", "replace"))
                cache[url] = (response.status, response.geturl(), parser)
        except urllib.error.HTTPError as error:
            cache[url] = (error.code, url, None)
        except (urllib.error.URLError, TimeoutError):
            cache[url] = (None, url, None)
    return cache[url]


def check(url, cache):
    problems = []
    status, final_url, page = fetch(url, cache)
    if status != 200 or page is None:
        return [f"strona zwraca kod {status}" if status else "strona nie odpowiada"]
    if final_url != url:
        problems.append(f"przekierowuje na {final_url}")
    if page.canonical is None:
        problems.append("brak canonical")
    elif page.canonical != url:
        problems.append(f"canonical wskazuje {page.canonical}")
    if not page.alternates:
        return problems + ["brak znaczników hreflang"]
    if url not in page.alternates.values():
        problems.append("brak hreflang wskazującego tę samą stronę")
    for code, alternate in page.alternates.items():
        region = code.split("-")[-1] if code.count("-") else ""
        if not CODE.match(code) or region in RESERVED_REGIONS:
            problems.append(f"nieprawidłowy kod {code}")
        if not alternate.startswith(("https://", "http://")):
            problems.append(f"{code}: adres względny {alternate}")
            continue
        alt_status, alt_final, alt_page = fetch(alternate, cache)
        if alt_status != 200 or alt_page is None:
            reason = f"zwraca kod {alt_status}" if alt_status else "nie odpowiada"
            problems.append(f"{code}: {alternate} {reason}")
        elif alt_final != alternate:
            problems.append(f"{code}: {alternate} przekierowuje na {alt_final}")
        elif url not in alt_page.alternates.values():
            problems.append(f"{code}: {alternate} nie wskazuje z powrotem tej strony")
    return problems


cache = {}
failed = 0
for url in sys.argv[1:]:
    problems = check(url, cache)
    print(url)
    for problem in problems or ["OK"]:
        print(f"  {problem}")
    failed += bool(problems)
sys.exit(1 if failed else 0)

Uruchomiony na trzech wersjach podstrony, z których niemiecka ma błędy, daje taki wynik:

wynik.txt
https://www.example.com/pl/oferta
  de: https://www.example.com/de/angebot nie wskazuje z powrotem tej strony
https://www.example.com/en/offer
  OK
https://www.example.com/de/angebot
  canonical wskazuje https://www.example.com/pl/oferta
  nieprawidłowy kod en-uk

Skrypt sprawdza znaczniki w HTML-u, a nie w mapie strony ani w nagłówkach HTTP, i porównuje adresy dosłownie, więc /en/offer i /en/offer/ uzna za różne. To celowe, bo dla Google to też są różne adresy. Listę adresów do sprawdzenia najprościej wziąć z mapy strony. Kod wyjścia różny od zera pozwala dodać skrypt do sprawdzania przy każdym wdrożeniu, zanim błąd zobaczy Google.

Wpływ wersji językowych na SEO polskiej strony

Dodanie wersji angielskiej nie powinno odbierać ruchu polskiej, bo obie wersje odpowiadają na zapytania w innych językach, a przetłumaczona treść nie jest duplikatem. Ryzyko leży gdzie indziej: w zmianie adresów polskiej wersji przy okazji dodawania języków.

Jeśli polska strona działa dziś pod adresami bez prefiksu, a nowa struktura przewiduje /pl/ dla wszystkich polskich podstron, to wszystkie dotychczasowe adresy się zmieniają. To jest pełna migracja: z inwentaryzacją, mapą przekierowań i monitoringiem, które opisaliśmy we wpisie o tym, jak przeprowadzić migrację strony bez utraty pozycji. Alternatywą jest zostawienie polskiej wersji pod dotychczasowymi adresami i dodanie tylko nowych podkatalogów dla innych języków. Struktura jest wtedy mniej symetryczna, ale nie wymaga przenoszenia stron, które już mają pozycje.

Każdą wersję warto obserwować osobno. W raporcie skuteczności Search Console można filtrować strony po fragmencie adresu, na przykład /en/, i zestawiać wyniki w zakładce Kraje. Można też dodać osobną usługę z prefiksem adresu dla każdego podkatalogu, która obejmuje tylko adresy zaczynające się od tego prefiksu.

Ostatnia rzecz to utrzymanie. Każda nowa podstrona i każda zmiana treści w polskiej wersji to pytanie, kiedy pojawi się w pozostałych. Bez ustalonego procesu wersje z czasem się rozjeżdżają: polska ma nowe ceny i usługi, angielska opisuje ofertę sprzed roku. Lepiej mieć dwie aktualne wersje językowe niż pięć, z których trzech nikt nie pilnuje.

Najczęstsze pytania o stronę wielojęzyczną i hreflang

Czy hreflang jest potrzebny, jeśli mam tylko dwie wersje językowe?

Nie jest obowiązkowy, ale przy dwóch wersjach jego wdrożenie to kilka linii w szablonie, a pomaga Google pokazać właściwą wersję właściwym użytkownikom. Bez niego angielska wersja może pojawiać się w wynikach dla użytkowników szukających po polsku i odwrotnie.

Czy angielska wersja strony to duplikat polskiej?

Nie. Google traktuje zlokalizowane wersje jako duplikaty tylko wtedy, gdy główna treść nie została przetłumaczona. Duplikatem jest dopiero wersja z przetłumaczonym menu i polskim tekstem.

Subdomena czy podkatalog: co jest lepsze dla SEO strony wielojęzycznej?

Google opisuje obie struktury jako poprawne. Podkatalogi są prostsze we wdrożeniu i utrzymaniu, a wszystkie wersje korzystają z jednej domeny. Subdomeny mają sens, gdy wersje działają na różnych serwerach albo zarządzają nimi różne zespoły. Google odradza tylko parametry w adresie.

Czy automatycznie przekierowywać według języka przeglądarki?

Nie. Google radzi unikać automatycznych przekierowań między wersjami językowymi, bo robot łączy się zwykle z adresów w USA i bez preferencji językowej, więc mógłby nie zobaczyć pozostałych wersji. Zamiast przekierowania pokaż podpowiedź z linkiem do wersji w języku przeglądarki.

Czy x-default jest obowiązkowy?

Nie. To wskazanie strony dla użytkowników, których język nie pasuje do żadnej wersji. Przydaje się, gdy masz stronę wyboru języka albo jedną wersję, która ma być domyślna, na przykład angielską.

Czy atrybut lang jest potrzebny, skoro Google go nie używa?

Tak. Google rozpoznaje język z treści, ale czytniki ekranu i przeglądarki korzystają z atrybutu lang, a jego ustawienie jest wymaganiem WCAG na poziomie A.

Od czego zacząć

Zanim powstanie pierwsza przetłumaczona podstrona, zapisz trzy decyzje: języki czy kraje, struktura adresów i lista podstron, które w każdym języku zostaną przetłumaczone w całości. Te trzy punkty warto mieć w briefie projektu, bo od nich zależą wycena, routing i mapa strony. Pozostałe elementy techniczne, które sprawdza się przed startem każdej strony, zebraliśmy w liście kontrolnej SEO technicznego.

Piszemy oprogramowanie od 2020 roku, a po oddaniu projektu dostajesz prawa do kodu i dokumentację. Jak wygląda współpraca od briefu do startu, opisujemy na stronie Jak pracujemy, a zakres usług jest w ofercie. Jeśli planujesz wersje językowe albo istniejące nie działają w Google tak, jak powinny, napisz do nas.