W części pierwszej oceniłem semantykę HTML nowego modułu Table of Contents na 8,5/10. Ta ocena była uczciwa — i oparta na niewłaściwym dokumencie. Ten wpis jest o tym, jak się o tym dowiedziałem, co dokładnie moduł mówi maszynom czytającym stronę, i dlaczego ocena agent-readiness wychodzi zupełnie inaczej niż ocena semantyki.
Skąd część druga
Nie planowałem jej. Wymusił ją incydent — najlepszy możliwy rodzaj incydentu, bo tani: AI czytające moje własne serwisy zwróciło uwagę, że na wpisach pełnych nagłówków H2 spis treści komunikuje „No headings found in this post”. Dla mnie, w przeglądarce, spis działał bez zarzutu: lista, linki, podświetlanie aktywnej sekcji. Dla maszynowego czytelnika spis nie istniał — istniał za to jego nekrolog.
W części pierwszej chwaliłem markup: nav, ol, aria-current, ukryty przed czytnikami ekranowymi marker numeracji. Wszystko to prawda. Problem w tym, skąd ten markup pochodził. Analizowałem DOM działającej strony — czyli stan po wykonaniu JavaScriptu. A dokument, który serwer wysyła w pierwszym bajcie i który czyta każdy crawler, wygląda inaczej.
Dowód: jeden curl
Surowy HTML wpisu, bez wykonywania JavaScriptu:
curl -s https://twojadomena.pl/twoj-wpis/ | grep -A5 'table_of_contents'
Wynik (skrócony do sedna):
<div class="et_pb_table_of_contents ...">
<h2 class="et_pb_table_of_contents__title">Table of Contents</h2>
<nav class="et_pb_table_of_contents__nav" aria-label="Table of contents">
<div class="et_pb_table_of_contents__empty">
<p>No headings found in this post.</p>
</div>
<ol class="et_pb_table_of_contents__list ..."></ol>
</nav>
</div>
Dwie rzeczy, obie istotne:
Lista jest pusta. <ol></ol> — zero elementów. Nagłówki wykrywa dopiero skrypt w przeglądarce; w payloadzie z serwera znajduje się nawet jego konfiguracja:
var diviModuleTableOfContentsData = [{"selector":"...","data":{
"source":{"desktop":{"value":{"includedHeadings":["h1","h2","h3"], ...
Czyli: które nagłówki zbierać, wie serwer — ale zbiera je klient. Spis treści powstaje wyłącznie w przeglądarce z włączonym JavaScriptem.
Fallback jest jawny. W części pierwszej komunikat pustego stanu miał atrybuty hidden aria-hidden="true". W surowym HTML-u nie ma żadnego z nich. To JavaScript, po zbudowaniu listy, chowa komunikat i dokłada atrybuty — dlatego w hydratowanym DOM wszystko wyglądało wzorowo. W initial payloadzie komunikat „No headings found in this post” jest zwykłym, widocznym akapitem treści.
Co to znaczy dla maszynowego czytelnika
Crawlery i pipeline’y ekstrakcji, z których korzystają systemy AI, w ogromnej większości czytają initial payload — bez wykonywania JavaScriptu albo z jego ograniczonym budżetem. Taki czytelnik dostaje dokument, który:
- nie zawiera spisu treści — mimo że wizualnie strona go ma,
- zawiera za to fałszywą deklarację o strukturze — „brak nagłówków w tym wpisie”, stojącą obok faktycznych nagłówków H2 i H3.
Punkt drugi jest gorszy od pierwszego. Brak spisu to utracona pomoc nawigacyjna — szkoda, ale przeżyjemy. Fałszywa deklaracja to aktywny szum semantyczny: dokument twierdzi coś nieprawdziwego o własnej strukturze. Większość modeli zignoruje to jako śmieć interfejsowy. Część — szczególnie przy zadaniach oceny struktury dokumentu — potraktuje dosłownie. Nie kontrolujesz, która część Twoich maszynowych czytelników trafi do której grupy.
Jest jeszcze punkt trzeci, drobniejszy: nawet w wersji hydratowanej linki spisu mają href="#", a docelową kotwicę trzymają w atrybucie data-et-toc-link, obsługiwanym przez JS. Pisałem o tym w części pierwszej jako o „mniej eleganckim” rozwiązaniu. W kontekście agent-readiness to nie kwestia elegancji: agent, który mimo wszystko doczytałby listę, dostaje linki prowadzące donikąd.
Ocena agent-readiness
Semantykę markupu oceniałem w części pierwszej i tamta ocena — 8,5/10 dla hydratowanego DOM — może zostać. Agent-readiness to osobna rubryka i osobne kryteria:
Obecność w initial payloadzie: 0/10. Spis treści nie istnieje w dokumencie wysyłanym przez serwer.
Prawdziwość komunikatów: poniżej zera. Moduł nie tylko nie pomaga — wprowadza w błąd. Pusty stan bez hidden w payloadzie to fałszywy sygnał o braku struktury na stronach, które strukturę mają.
Nawigowalność dla agenta: 1/10. href="#" + data-et-toc-link oznacza, że nawet po hipotetycznym wykonaniu JS linki nie są klasycznymi kotwicami, które agent może zrozumieć i wykorzystać bez interpretowania customowego skryptu.
Koszt naprawy po stronie Elegant Themes: niski. I to jest najbardziej frustrująca część tej oceny. Treść wpisu jest znana serwerowi w momencie renderowania — PHP ma pełny dostęp do nagłówków. Nie ma technicznego powodu, żeby listę budować client-side; to decyzja architektoniczna odziedziczona po visual builderze, w którym wszystko jest komponentem JS. Lista mogłaby przychodzić z serwera gotowa, a JavaScript zostać tam, gdzie faktycznie jest potrzebny: smooth scroll i aria-current przy przewijaniu.
Agent-readiness modułu: 2/10. Dwa punkty za to, że kontener nav z aria-label w payloadzie jest — szkielet stoi, tylko treści w nim brak.
Co powinno zrobić Elegant Themes
W kolejności od rozwiązania właściwego do minimum higieny:
- Render listy server-side. Nagłówki są znane w PHP; lista
<ol>z klasycznymi kotwicamihref="#id-sekcji"powinna przychodzić w initial payloadzie. JS zostaje dla interakcji, nie dla treści. - Jeśli nie server-side — odwrócić logikę pustego stanu. Komunikat „brak nagłówków” powinien być wysyłany z atrybutem
hiddeni odsłaniany przez JS tylko wtedy, gdy nagłówków faktycznie brak. Dziś jest odwrotnie: fałszywa deklaracja domyślnie jawna, prawda dopiero po hydratacji. - Klasyczne kotwice w
href. Postulat z części pierwszej, teraz z mocniejszym uzasadnieniem.
Co możesz zrobić dziś
Zmień Empty State Message na prawdziwy. Pola nie da się użyć do naprawy problemu, ale da się nim usunąć kłamstwo. Zamiast domyślnego „No headings found in this post” ustaw komunikat zgodny ze stanem faktycznym, np. „Spis treści wczytuje się w przeglądarce”. Dla maszynowego czytelnika to nadal brak spisu — ale już bez fałszywej deklaracji o strukturze dokumentu. Z szumu aktywnego robi się szum neutralny.
Rozważ spis renderowany server-side. Klasyczne wtyczki ToC dla WordPressa budują listę w PHP — mniej urodziwe od modułu Divi, ale ich spis istnieje w pierwszym bajcie. Na wpisach, gdzie struktura ma znaczenie (przewodniki, długie analizy), to wymiana warta rozważenia.
Sprawdź własną stronę tym samym curlem. Jedna linijka wyżej. Jeśli zobaczysz pustą <ol> i jawny fallback — właśnie dowiedziałeś się czegoś, czego nie pokaże żaden audyt SEO.
Lekcja szersza niż jeden moduł
Metodologiczny błąd części pierwszej wart jest nazwania wprost, bo nie popełniłem go tylko ja: oceniałem DOM, a trzeba było oceniać payload. DevTools, „zbadaj element”, kopiowanie markupu z działającej strony — wszystko to pokazuje stan po hydratacji. Maszynowy czytelnik widzi stan sprzed niej. Między jednym a drugim siedzi JavaScript, i to on decyduje, czy Twoja strona mówi agentom prawdę.
Najciekawsze w tej historii jest to, że strona może być jednocześnie: poprawna semantycznie (w DOM), dostępna (dla czytników ekranowych z JS), świetna wizualnie — i kłamiąca dla maszyn. Żadne z klasycznych narzędzi — walidator HTML, audyt Lighthouse, audyt SEO — tego nie wyłapie, bo każde z nich patrzy na inną warstwę. Rozjazd między initial payloadem a hydratowanym DOM to osobna kategoria problemu i osobna kategoria audytu.
Ten wpis zaczął się od tego, że AI przeczytało moją stronę i zobaczyło co innego niż ja. Jeden moduł, jeden fałszywy komunikat — znaleziony przypadkiem. Pytanie, na które przypadkiem odpowiedzi nie dostaniesz: co jeszcze Twoja strona mówi maszynom, kiedy nie patrzysz?
Dokładnie to sprawdza raport agent-readiness na Studio.iFox.pl — porównujemy to, co wysyła Twój serwer, z tym, co widzi użytkownik w przeglądarce, i pokazujemy każde miejsce, w którym te dwie wersje Twojej strony mówią co innego. ToC z tego wpisu to jeden z checków; pozostałe znajdują rzeczy, o których istnieniu autorzy stron zwykle nie wiedzą, bo w przeglądarce wszystko wygląda dobrze.











