Tutorial: JSON-LD, który wyszukiwarki naprawdę czytają

Spis treści
Dane strukturalne nie mają stanu błędu, który ktokolwiek by zauważył. Strona ze względną ścieżką obrazu, datą w zapisie lokalnym albo ceną z symbolem waluty jest serwowana dokładnie tak samo jak poprawna, plasuje się dokładnie tak samo jak poprawna i po prostu nigdy nie dostaje wyniku rozszerzonego, dla którego ją napisano. Znaczniki są, crawler je czyta i nic się nie dzieje.
To czyni z danych strukturalnych raczej problem utrzymania niż pisania. Błędy są mechaniczne, powtarzają się na każdej stronie powstałej z tego samego szablonu, a jeden niewłaściwy znak w polu, którego żaden odwiedzający nigdy nie widzi, rozstrzyga, czy wpis pokaże ocenę, czy goły niebieski odnośnik. Poniżej opisany jest łańcuch, przez który musi przejść fragment znaczników, oraz cztery miejsca, w których zwykle z niego wypada.

Dwa wiersze, na których wszystko się opiera
Zanim odczytana zostanie choćby jedna właściwość, dwa pola rozstrzygają, czy blok w ogóle zostanie obejrzany. @context musi wskazywać na https://schema.org; bez tego obiekt jest kawałkiem JSON bez słownika, a każda właściwość w nim nic nie znaczy. @type nazywa następnie opisywaną rzecz. Zapis ma przy tym znaczenie: Article jest typem, article nim nie jest, i różnica ta nigdzie nie wywołuje ostrzeżenia.
Tam, gdzie do jednej strony należy kilka obiektów – artykuł, jego autor i ścieżka okruszków – mogą stać w jednym bloku pod @graph albo w osobnych elementach skryptu. Oba sposoby działają. Nie działają natomiast dwa bloki opisujące tę samą rzecz odmiennie, ponieważ parser nie ma reguły rozstrzygającej, o który z nich chodzi.
Cztery błędy, które nie dają komunikatu
Na te cztery przypada większość znaczników, które są poprawne, a mimo to nic nie pokazują. Łączy je jedna cecha: dane pole jest dla odwiedzających niewidoczne, więc nikt nie zauważa, że jest błędne.
| Zapisane tak | Co się dzieje | Jak być powinno |
|---|---|---|
"image": "/img/a.jpg" |
Ścieżka nigdy nie zostaje rozwiązana, obraz uchodzi za brakujący, a wraz z nim znika najbardziej widoczna część wyniku. | Pełny adres wraz ze schematem. |
"datePublished": "26.08.2026" |
Pole zostaje pominięte. Artykuł bez daty publikacji traci datę we wpisie. | ISO 8601: 2026-08-26 albo z godziną i przesunięciem. |
"price": "49,90 EUR" |
Oferta zostaje odrzucona w całości – cena i dostępność znikają razem. | Sama liczba z kropką, waluta w priceCurrency. |
"@type": "article" |
Typ jest nieznany, każda właściwość poniżej zostaje odczytana jako rzecz nieokreślona. | Dokładnie Article, z wielką literą włącznie. |
Pierwsze trzy pochodzą z szablonów, które wypełniają znaczniki tymi samymi zmiennymi co samą stronę: ścieżka obrazu jest względna, bo szablon zapisuje ścieżki względne, data jest sformatowana, bo sformatowana data i tak była pod ręką, a cena niesie swoją walutę, bo tak się ją drukuje. Dane strukturalne potrzebują wartości surowych, a nie przygotowanych do wyświetlenia.
Article: jedno pole obowiązkowe i cztery, które mają znaczenie
Artykuł potrzebuje pola headline i niczego więcej. Wszystko, co sprawia, że wynik wygląda jak wynik, jest dobrowolne – i właśnie dlatego tak wiele znaczników przechodzi kontrolę i nic nie pokazuje.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "JSON-LD, który wyszukiwarki naprawdę czytają",
"description": "Dlaczego dane strukturalne zawodzą bez komunikatu o błędzie.",
"image": ["https://www.example.com/images/json-ld.png"],
"author": { "@type": "Person", "name": "Lukas Wojcik" },
"datePublished": "2026-08-26",
"dateModified": "2026-08-26",
"mainEntityOfPage": { "@type": "WebPage", "@id": "https://www.example.com/blog/json-ld/" }
}
</script>
Cztery drobiazgi rozstrzygają, czy te znaczniki cokolwiek przyniosą. Pole headline zostaje ucięte powyżej mniej więcej 110 znaków, więc tytuł złożony z tytułu strony i nazwy witryny traci zakończenie. Pole author należy do obiektu z @type; sam ciąg znaków zostanie przyjęty, ale nic nie mówi o tym, czy pisała osoba, czy organizacja. Pole dateModified nie może być wcześniejsze niż datePublished – kombinacja pojawiająca się zawsze wtedy, gdy migracja wpisze datę importu do jednego z tych pól. A obraz powinien istnieć w więcej niż jednej proporcji, bo układ, w którym ląduje wynik, nie jest tym, o który proszą znaczniki.
FAQPage: reguła sięgająca aż na stronę
Blok FAQ to jedyny typ, którego poprawność zależy od czegoś poza samymi znacznikami: każde pytanie i każda odpowiedź muszą być widoczne na samej stronie. Znaczniki opisujące listę rozwijaną, która nigdy się nie otwiera, albo pytania istniejące wyłącznie w JSON, uchodzą za wprowadzające w błąd, a nie za niekompletne – a to działanie ręczne, a nie brakująca funkcja.
Od 2023 roku sam wynik rozszerzony FAQ pojawia się niemal wyłącznie przy witrynach urzędowych i medycznych, dla większości stron widoczna nagroda więc odpada. Znaczniki i tak się opłacają, tylko innym czytelnikom: wyszukiwarce wewnętrznej, asystentom czytającym stronę, aby odpowiedzieć na pytanie, i każdemu dalszemu systemowi, który inaczej musiałby zgadywać, która część tekstu jest pytaniem. Kto oczekuje po tym wpisu z gwiazdkami i strzałką, ten się zawiedzie; kto chce maszynowo czytelnej pary pytanie-odpowiedź, dostaje dokładnie to.
Product: cena, waluta, dostępność
Produkt bez bloku offers to produkt bez ceny, a cena jest jedynym powodem istnienia wyniku produktowego.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Kompaktowy switch PoE, 8 portów",
"image": ["https://www.example.com/images/switch.png"],
"brand": { "@type": "Brand", "name": "Example" },
"offers": {
"@type": "Offer",
"price": "149.00",
"priceCurrency": "EUR",
"availability": "https://schema.org/InStock",
"url": "https://www.example.com/switch/"
}
}
</script>
Pole availability przyjmuje adres schema.org, a nie samo słowo: InStock w oderwaniu nie jest wartością. Gdy dochodzi aggregateRating, potrzebuje zarówno ratingValue, jak i liczby ocen – jedno bez drugiego odpada, a ocena widniejąca na stronie, ale nieobecna w znacznikach, jest dla wpisu bez wartości. Oceny, których strona w ogóle nie potrafi pokazać, odwrotnie, nie mają w znacznikach czego szukać.
Sprawdzenie, zanim trafi na produkcję
Trzy kontrole wychwytują niemal wszystko i odpowiadają na różne pytania. Kontrola strukturalna czyta blok samodzielnie: czy są pola obowiązkowe, czy data jest datą ISO, czy cena jest liczbą. Rich Results Test mówi następnie, czy Google w ogóle rozpoznaje typ – to jedyne miejsce, w którym widać @type zapisane małą literą. A raporty ulepszeń w Search Console pokazują, co dzieje się w skali całej witryny, i tam błąd szablonu ujawnia się jako kilkaset identycznych ostrzeżeń, a nie jako jedno.
Żadna z nich nie odpowiada natomiast na pytanie, czy wynik zostanie pokazany. Poprawne znaczniki są warunkiem, a nie roszczeniem: Google rozstrzyga przy każdym zapytaniu i każdej stronie, czy pojawi się wynik rozszerzony, a poprawny blok, z którego nic nie wynika, jest przypadkiem normalnym, a nie usterką. Powód, dla którego pisze się dane strukturalne, to ten, który tę decyzję przetrwa – strona, której twierdzenia są czytelne dla maszyn, w chwili, gdy coraz więcej z nich czyta.
Pytania i odpowiedzi
Co się dzieje, gdy zarówno wtyczka SEO, jak i motyw generują własny blok Article?
Wtedy dwa bloki opisują tę samą rzecz, i to zwykle nie jednakowo: wtyczka bierze na przykład tytuł SEO i obraz w pełnym rozmiarze, motyw tytuł wpisu i miniaturę, a daty różnią się o strefę czasową. Na dokładnie taki przypadek parser nie ma reguły rozstrzygającej, o który blok chodzi. Komunikatu o błędzie mimo to nie ma; Rich Results Test pokazuje jedynie dwa wykryte elementy typu Article.
Rozwiązaniem jest generowanie znaczników z jednego źródła. Wtyczka SEO często już tworzy @graph obejmujący witrynę, stronę, autora i ścieżkę okruszków. W takim przypadku lepiej pozostawić jej znaczniki, a wyłączyć generowanie ich przez motyw. Jeśli motyw ma coś uzupełnić, powinien odwołać się przez @id do artykułu w grafie wtyczki, zamiast tworzyć drugi artykuł.
Po każdej zmianie wtyczki albo motywu warto zajrzeć do źródła strony wpisu i policzyć wystąpienia application/ld+json. Dwa bloki nie są błędem; dwa bloki opisujące tę samą stronę odmiennie już tak.
Czy Google czyta JSON-LD wstawiany dopiero przez JavaScript, na przykład przez Tag Managera?
Google potrafi go odczytać, bo jego crawler renderuje strony, zanim oceni dane strukturalne, a Rich Results Test pokazuje również bloki wstawione w ten sposób. Renderowanie odbywa się jednak w osobnym, późniejszym kroku, a blok wstawiany dopiero po działaniu użytkownika albo po udzieleniu zgody nigdy do crawlera nie dociera.
Dla pozostałych odbiorców, takich jak asystenci i dalsze systemy wymienione przy typie FAQ, sytuacja wygląda gorzej. Wiele maszyn zbierających strony dla modeli językowych albo tworzących podglądy nie wykonuje JavaScriptu i widzi tylko dostarczony HTML. Blok z Tag Managera dla nich nie istnieje. Dane strukturalne, które mają dawać coś więcej niż pojedynczy wynik rozszerzony, należą więc do HTML generowanego po stronie serwera.
Jak sprawić, by szablon od razu dawał datę i cenę we właściwym formacie?
Formatując surową wartość specjalnie dla znaczników, zamiast przejmować wersję przygotowaną do wyświetlenia. W PHP date('c', $znacznik_czasu) zwraca datę ISO 8601 z godziną i przesunięciem, a w WordPressie get_the_date('c') robi to samo dla wpisu. Dla ceny number_format($cena, 2, '.', '') daje samą liczbę z kropką i bez separatora tysięcy, czyli 49.90 zamiast 49,90 EUR.
W JavaScripcie toISOString() również tworzy poprawną datę, ale zawsze w UTC z doklejonym Z. To poprawne, tyle że wpis opublikowany tuż po północy czasu lokalnego nosi wtedy jeszcze datę poprzedniego dnia.
Jakiej liczby potrzebuje aggregateRating oprócz ratingValue?
Albo ratingCount, czyli liczby oddanych ocen, albo reviewCount, czyli liczby recenzji; wystarczy jedno z nich. Jeśli skala odbiega od przedziału od jednego do pięciu, do znaczników należą także bestRating i worstRating; w przeciwnym razie obowiązuje skala pięciostopniowa, a wartość 8 ze skali dziesięciostopniowej się w niej nie mieści.