Wtyczka do danych strukturalnych to najszybsza droga do poprawnego znacznika Organization, ale rzadko najtańsza. Typowy pakiet dokłada do każdej podstrony kilkadziesiąt kilobajtów JSON-LD, dubluje encje już generowane przez motyw i w praktyce nie da się go przyciąć do tego, czego naprawdę potrzebuje wyszukiwarka. W tym tutorialu pokazujemy, jak dodać dwa bloki schematu, Organization dla całej witryny i Product dla kart produktowych, ręcznie w motywie potomnym: z kontrolą nad polami, walidacją i monitoringiem błędów.
Kiedy warto zrezygnować z wtyczki
Własny kod ma sens w trzech sytuacjach. Pierwsza: witryna już emituje JSON-LD z motywu lub z WooCommerce, a wtyczka dokłada drugą encję tego samego typu. Google wybiera wtedy jedną z nich w sposób, którego nie kontrolujesz. Druga: potrzebujesz pól, których interfejs wtyczki nie udostępnia, na przykład hasMerchantReturnPolicy albo shippingDetails z realnymi stawkami. Trzecia: masz nietypowy typ wpisu (custom post type) z danymi w polach własnych i mapowanie przez interfejs wtyczki jest bardziej pracochłonne niż dwadzieścia linii PHP.
Sytuacja odwrotna też istnieje. Jeśli witrynę utrzymuje osoba bez dostępu do kodu, a schemat trzeba zmieniać co kwartał, wtyczka wygrywa na obsłudze. Decyzję najlepiej podjąć raz, zapisać ją w dokumentacji technicznej i trzymać się jej przy kolejnych wdrożeniach. Jeżeli właśnie przenosisz sklep, zrób to przed migracją, a nie po: kolejność kroków opisujemy w checkliście migracji sklepu na nową platformę.
| Kryterium | Wtyczka | Własny kod |
|---|---|---|
| Czas wdrożenia | 15 minut | 1–3 godziny |
| Kontrola nad polami | tylko to, co w interfejsie | pełna |
| Waga kodu na stronie | 20–60 kB JSON-LD | 1–4 kB |
| Ryzyko duplikatów encji | wysokie | kontrolowane |
| Utrzymanie | aktualizacje wtyczki | Twój zespół |
Gdzie umieścić kod w motywie potomnym
Schemat wstrzykujemy w sekcję head przez hook wp_head, opisany w dokumentacji WordPressa dla deweloperów. Priorytet 20 wystarczy, żeby znacznik pojawił się po tagach kanonicznych wtyczki SEO. Kod trafia do motywu potomnego, nigdy do plików motywu nadrzędnego, bo pierwsza aktualizacja go usunie.
Praktyczna struktura: katalog inc/schema/ w motywie potomnym, osobny plik na każdy typ encji i jedno wymaganie w functions.php. Dzięki temu wyłączenie bloku Product sprowadza się do zakomentowania jednej linii, a nie do grzebania w pliku na czterysta linii. Jeśli nie chcesz ruszać motywu, ten sam kod zadziała w małej wtyczce typu mu-plugin, co ma dodatkową zaletę: przetrwa zmianę szablonu.
Blok Organization dla całej witryny
Organization opisuje wydawcę, nie stronę. Wystarczy jedna instancja, najlepiej na stronie głównej, z własnym identyfikatorem @id, do którego mogą się odwoływać inne encje. Pola obowiązkowe są skromne: nazwa, adres URL i logo. Resztę dobieramy pod realne potrzeby, a nie pod długość JSON-a.
add_action( 'wp_head', 'firma_organization_schema', 20 );
function firma_organization_schema() {
if ( ! is_front_page() ) {
return;
}
$data = array(
'@context' => 'https://schema.org',
'@type' => 'Organization',
'@id' => home_url( '/#organization' ),
'name' => get_bloginfo( 'name' ),
'url' => home_url( '/' ),
'logo' => array(
'@type' => 'ImageObject',
'url' => 'https://przyklad.pl/wp-content/uploads/logo.png',
'width' => 512,
'height' => 512,
),
'sameAs' => array(
'https://www.linkedin.com/company/przyklad',
'https://www.youtube.com/@przyklad',
),
);
echo '<script type="application/ld+json">'
. wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
. '</script>' . "n";
}Dwie rzeczy, które łatwo przeoczyć. wp_json_encode sam escape’uje znaki specjalne, więc nie dokładamy esc_html na gotowym JSON-ie (to psuje strukturę). Flaga JSON_UNESCAPED_UNICODE jest po to, żeby polskie znaki nie zamieniły się w ciąg ł: technicznie to poprawny JSON, ale nieczytelny w debugowaniu. Logo musi mieć co najmniej 112 pikseli po krótszej krawędzi, inaczej Google je zignoruje.
Blok Product na stronie produktu
Product jest wymagający: bez offers z ceną i walutą Google traktuje znacznik jako niekompletny i nie pokaże wyniku rozszerzonego. Warunek is_singular ograniczamy do właściwego typu wpisu, a brak ceny traktujemy jako powód do całkowitego pominięcia bloku. Pusty price jest gorszy niż brak schematu, bo generuje błąd w raporcie.
add_action( 'wp_head', 'firma_product_schema', 20 );
function firma_product_schema() {
if ( ! is_singular( 'produkt' ) ) {
return;
}
$id = get_queried_object_id();
$price = get_post_meta( $id, '_cena_brutto', true );
if ( '' === $price ) {
return;
}
$data = array(
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => get_the_title( $id ),
'description' => wp_strip_all_tags( get_the_excerpt( $id ) ),
'sku' => get_post_meta( $id, '_sku', true ),
'brand' => array( '@type' => 'Brand', 'name' => 'Przyklad' ),
'offers' => array(
'@type' => 'Offer',
'url' => get_permalink( $id ),
'price' => number_format( (float) $price, 2, '.', '' ),
'priceCurrency' => 'PLN',
'availability' => 'https://schema.org/InStock',
),
);
echo '<script type="application/ld+json">'
. wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE )
. '</script>' . "n";
}Cena idzie do JSON-a jako liczba z kropką dziesiętną, bez symbolu waluty i bez separatora tysięcy. Dostępność to pełny URI ze słownika schema.org, nie słowo „dostępny”. Jeśli planujesz schemat również pod cytowania w modelach językowych, zestaw pól różni się od minimum wymaganego przez Google: rozkładamy to na części w materiale o Product schema pod AI.
Pobieranie danych z pól własnych
Najczęstsza awaria tego podejścia nie dotyczy schematu, lecz źródła danych. get_post_meta zwraca pusty ciąg zarówno wtedy, gdy pole nie istnieje, jak i wtedy, gdy ktoś je wyczyścił, więc każde pole krytyczne wymaga jawnego sprawdzenia. Przy polach z ACF pamiętaj, że klucz w bazie ma prefiks, a get_field zwraca już przetworzoną wartość (na przykład tablicę dla pola relacji), której nie wolno wrzucać prosto do JSON-a.
Repeatery i pola wielokrotne wymagają decyzji, co robić z brakami. Rozsądna reguła: buduj tablicę warunkowo i dokładaj klucz tylko wtedy, gdy wartość przeszła walidację. Dzięki temu JSON-LD ma zawsze mniej pól, niż mógłby mieć, ale żadne z nich nie jest puste. Ten sam schemat myślenia stosujemy przy gtin13, mpn i aggregateRating: ocena bez liczby opinii to gwarantowany błąd w walidatorze.
Osobny problem to buforowanie. Jeśli witryna stoi za pełnym cache strony (LiteSpeed, Varnish, warstwa CDN), zmiana w polu własnym nie zmieni JSON-LD do momentu wyczyszczenia cache dla danego adresu. W praktyce oznacza to, że po każdej masowej aktualizacji cennika trzeba wyczyścić pamięć podręczną szablonu produktu, inaczej Google przez kilka dni czyta starą cenę ze strony, podczas gdy klient widzi nową. Rozbieżność ceny w znaczniku i ceny w treści to jedna z niewielu rzeczy, za które Google wyłącza wyniki rozszerzone dla całej domeny, więc warto wpisać ten krok do procedury aktualizacji oferty.
Walidacja i testy
Kolejność testów ma znaczenie. Najpierw sprawdzamy, czy JSON jest w ogóle poprawny składniowo (wystarczy wklejenie fragmentu do dowolnego parsera), potem uruchamiamy test wyników z elementami rozszerzonymi Google, a na końcu walidator schema.org, który wyłapie pola poprawne składniowo, ale niezgodne ze słownikiem. Dwa narzędzia, bo pokrywają różne klasy błędów: Google raportuje tylko to, co wpływa na jego funkcje.
Warto przetestować przynajmniej po jednej stronie z każdego wariantu: produkt z promocją, produkt niedostępny, produkt bez zdjęcia, strona główna. Lista wariantów i wyniki testów to naturalna kolumna w arkuszu kontrolnym; jeśli prowadzisz inwentaryzację szablonów, dopisz ją do szablonu audytu treści w Arkuszach Google i odhaczaj przy każdym wdrożeniu.
Testy warto też zautomatyzować na poziomie najprostszym, jaki wystarczy: skrypt, który raz na dobę pobiera dziesięć losowych adresów produktów, wyciąga z nich blok JSON-LD i sprawdza obecność czterech pól krytycznych (name, price, priceCurrency, availability). Nie zastępuje to walidatora, ale wychwytuje regresje w dniu, w którym powstały, a nie w miesiącu, w którym ktoś zajrzał do raportu. Koszt takiego skryptu to kilkadziesiąt linii i jedno zadanie cron.
Kontrola błędów w Search Console
Walidator mówi, co jest złego na jednej stronie. Raporty elementów rozszerzonych w Search Console mówią, ile stron jest dotkniętych i od kiedy, co jest ważniejsze. Po wdrożeniu spodziewaj się opóźnienia: pierwsze encje pojawiają się w raporcie po kilku dniach, pełne pokrycie po kilku tygodniach, bo zależy od tempa ponownego crawlu, a nie od tempa Twojego deployu.
Dwa wzorce alarmowe. Nagły skok ostrzeżeń typu „brak pola” zwykle oznacza zmianę w polach własnych albo import produktów bez którejś kolumny. Spadek liczby poprawnych encji przy stałej liczbie stron to najczęściej konflikt: druga wtyczka zaczęła emitować ten sam typ. Oba zjawiska dobrze widać w comiesięcznym zestawieniu; strukturę takiego raportu opisujemy w szablonie raportu miesięcznego dla klienta SEO. Zasady, które Google stosuje przy ocenie znaczników, zebrane są w wytycznych Search Central i warto je przeczytać przed pierwszym wdrożeniem, nie po karze.
Na koniec rzecz organizacyjna: dopisz do repozytorium krótki plik README z listą emitowanych typów, warunkami wyświetlania i źródłem każdego pola. Po pół roku nikt, łącznie z Tobą, nie będzie pamiętał, skąd bierze się priceValidUntil.
FAQ
Czy dane strukturalne bez wtyczki są traktowane inaczej przez Google?
Nie. Google czyta JSON-LD z sekcji head niezależnie od tego, co go wygenerowało. Liczy się poprawność składni i zgodność z wytycznymi, nie źródło kodu.
Czy muszę usunąć wtyczkę SEO, żeby dodać własny schemat?
Nie, ale musisz wyłączyć w niej emisję tych typów, które generujesz sam. Dwie encje Organization na jednej stronie to konflikt, który Google rozstrzygnie bez Twojego udziału.
Gdzie najlepiej trzymać kod: w motywie potomnym czy we wtyczce?
Motyw potomny wystarczy, jeśli nie planujesz zmiany szablonu. Mały mu-plugin jest bezpieczniejszy, bo schemat przetrwa przebudowę frontendu.
Co zrobić, gdy produkt nie ma ceny?
Pomiń cały blok Product. Znacznik bez pola price i priceCurrency zostanie zgłoszony jako błąd krytyczny i nie wygeneruje żadnego wyniku rozszerzonego.
Jak szybko zobaczę efekt w Search Console?
Pierwsze encje zwykle po 3–7 dniach, pełne pokrycie serwisu po 2–6 tygodniach. Tempo zależy od częstotliwości ponownego crawlu danego szablonu.
Czy AggregateRating można dodać bez realnych opinii?
Nie. Ocena musi być widoczna dla użytkownika na tej samej stronie i oparta na rzeczywistych opiniach. Znacznik bez widocznej treści narusza wytyczne i grozi ręczną karą.
