Inżynieria SEO na statycznej stronie — dane strukturalne, karty społecznościowe i sygnały dla crawlerów
seo,symfony,statyczna-strona,performanceCzęść 8 z 8
- 19 lat z WordPressem — dlaczego w końcu zrezygnowałem
- Symfony jako generator stron statycznych — jak działa holas.pl
- Zabezpieczenie jedynego dynamicznego endpointu strony statycznej — formularz kontaktowy
- Drzewo decyzyjne narzędzi dla Claude Code z globalną pamięcią
- Środowisko deweloperskie — od lokalnego devu do produkcji w dwóch kontenerach
- Budowanie holas.pl z AI — Claude Code, MCP i lokalne generowanie obrazów
- 4×100 w Lighthouse Mobile — co daje statyczna strona
- Inżynieria SEO na statycznej stronie — dane strukturalne, karty społecznościowe i sygnały dla crawlerów
holas.pl ma 100 punktów w kategorii SEO Lighthouse. Co to faktycznie sprawdza: czy tytuł meta jest obecny, czy opis meta jest obecny, czy canonical URL jest ustawiony, czy linki są dostępne dla crawlerów, czy strona jest mobilna. To minimalne wymagania — rzeczy, które blokują indeksowanie, gdy ich brakuje.
Czego Lighthouse SEO nie sprawdza: czy dane strukturalne są kompletne, jak strona renderuje się jako karta społecznościowa, co widzą czytniki RSS, czy Google może znaleźć i zaindeksować obrazy bez crawlowania każdej podstrony.
Ten wpis opisuje warstwę implementacyjną pod tym wynikiem.
Dane strukturalne
Dane strukturalne to JSON-LD w bloku <script type="application/ld+json">. Mówią wyszukiwarkom, czym jest strona — nie tylko co mówi. holas.pl używa czterech typów schematu.
WebSite + SearchAction
Każda strona zawiera schemat WebSite identyfikujący witrynę i jej punkt wyszukiwania:
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "holas.pl",
"url": "https://holas.pl",
"author": {
"@type": "Person",
"name": "Paweł Holik",
"url": "https://holas.pl"
},
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://holas.pl/search/?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
potentialAction umożliwia pole wyszukiwania w wynikach Google — pole wyszukiwania widoczne bezpośrednio w wynikach Google dla witryny. Kieruje do wyszukiwania opartego na Pagefind pod adresem /search/. To jedno dodatkowe pole w istniejącym schemacie bez żadnych wad.
BlogPosting
Wpisy na blogu mają najbogatszy schemat. Poza headline, description, url i datePublished, kilka pól wpływa na to, jak Google reprezentuje treść:
inLanguage—"en"lub"pl", wymagane do indeksowania wielojęzycznegowordCount— obliczany w czasie parsowania przezContentItem::wordCount()(usuwa tagi HTML, liczy tokeny)articleSection— kategoria wpisukeywords— tagi wpisu jako ciąg rozdzielony przecinkamiimage— zagnieżdżonyImageObjectzurl,widthiheight
{
"@type": "BlogPosting",
"headline": "Tytuł wpisu",
"inLanguage": "pl",
"wordCount": 842,
"articleSection": "porady",
"keywords": "seo, symfony, statyczna-strona",
"image": {
"@type": "ImageObject",
"url": "https://holas.pl/media/post-dir/featured.webp",
"width": 1280,
"height": 720
}
}
Bez ImageObject Google traktuje wyróżniony obraz jako nieznany załącznik. Z jawnie podanymi szerokością i wysokością obraz kwalifikuje się do dużych kart podglądu w Google Discover i Search.
BreadcrumbList
Google może zastąpić surowy URL w wynikach wyszukiwania nawigacją okruszkową — "Strona główna / Blog / porady / Tytuł wpisu". Wymaga to schematu BreadcrumbList.
Jest renderowany w breadcrumb.html.twig obok nawigacji HTML. Każdy okruszek to ListItem z position i item (URL). Ostatni element — bieżąca strona — ma nazwę, ale bez URL:
{
"@type": "BreadcrumbList",
"itemListElement": [
{ "@type": "ListItem", "position": 1, "name": "Strona główna", "item": "https://holas.pl/pl/" },
{ "@type": "ListItem", "position": 2, "name": "Wpisy", "item": "https://holas.pl/pl/wpisy/" },
{ "@type": "ListItem", "position": 3, "name": "porady", "item": "https://holas.pl/pl/wpisy/porady/" },
{ "@type": "ListItem", "position": 4, "name": "Tytuł wpisu" }
]
}
CollectionPage + ItemList
Strony z listingami kategorii, tagów i archiwum zawierają CollectionPage z zagnieżdżonym ItemList. Każdy wpis ma position i url. Renderowany tylko gdy listing zawiera wpisy — pusta strona kategorii tego nie dostaje.
{
"@type": "CollectionPage",
"name": "porady | holas.pl",
"mainEntity": {
"@type": "ItemList",
"numberOfItems": 5,
"itemListElement": [
{ "@type": "ListItem", "position": 1, "url": "https://holas.pl/pl/wpis/" }
]
}
}
Udostępnianie w mediach społecznościowych
Wymiary obrazu OpenGraph
Bez og:image:width i og:image:height platformy jak LinkedIn i Slack muszą pobrać obraz zanim wyrenderują kartę podglądu. Z nimi karta renderuje się natychmiast:
<!-- wpis na blogu (WebP, 1280×720) -->
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="720">
<meta property="og:image:type" content="image/webp">
<!-- inne strony (domyślny og:image, JPG, 1200×630) -->
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:type" content="image/jpeg">
Warunek jest w base.html.twig: jeśli obiekt content z obrazem jest zdefiniowany (wpis lub strona z wyróżnionym obrazem), użyj wymiarów WebP; w przeciwnym razie użyj wartości domyślnych dla og-default.jpg. Wyjątek dla JPG istnieje dlatego, że og:image jest odczytywany przez zewnętrzne crawlery, które nie obsługują WebP niezawodnie.
Karta Twitter/X
Podstawowy typ twitter:card był już obecny. Dodano trzy jawne pola:
<meta name="twitter:title" content="...">
<meta name="twitter:description" content="...">
<meta name="twitter:image" content="...">
Bez nich Twitter/X wraca do właściwości OG. Jawne meta usuwa tę zależność — jeśli przetwarzanie OG ma jakikolwiek problem, Twitter Card nadal ma poprawne wartości.
Meta article:*
Wpisy na blogu dostają specyficzne dla artykułu meta OG w bloku og_article_meta szablonu post.html.twig:
<meta property="article:published_time" content="2026-06-21T00:00:00+00:00">
<meta property="article:modified_time" content="2026-06-21T00:00:00+00:00">
<meta property="article:author" content="Paweł Holik">
<meta property="article:section" content="porady">
<meta property="article:tag" content="seo">
<meta property="article:tag" content="symfony">
<meta property="article:tag" content="statyczna-strona">
article:tag to jeden element na tag — nie oddzielony przecinkami ciąg. Specyfikacja Open Graph wymaga osobnych elementów dla właściwości wielowartościowych.
Czytniki RSS
content:encoded
Domyślny <description> RSS zawiera tylko fragment wpisu — pierwszy akapit z usuniętym HTML. content:encoded przenosi pełny HTML wpisu w bloku CDATA:
<content:encoded><![CDATA[<p>Pełna treść wpisu...</p>]]></content:encoded>
Wymaga to xmlns:content="http://purl.org/rss/1.0/modules/content/" na elemencie głównym <rss>. Czytniki RSS jak NetNewsWire, Reeder i Feedbin renderują content:encoded inline — subskrybenci czytają pełny artykuł bez opuszczania czytnika.
category i media:content
Każdy element RSS dostaje elementy <category> dla kategorii wpisu i każdego tagu:
<category>porady</category>
<category>seo</category>
<category>symfony</category>
media:content dołącza wyróżniony obraz jako typowany załącznik multimedialny:
<media:content url="https://holas.pl/media/post-dir/featured.webp"
medium="image" type="image/webp" width="1280" height="720"/>
Czytniki RSS renderujące obrazy inline (Feedly, Inoreader) używają tego do miniatury wpisu na liście. Wymaga to xmlns:media="http://search.yahoo.com/mrss/" na elemencie <rss>.
Sygnały dla crawlerów
max-image-preview:large
Domyślne zachowanie robots ogranicza podglądy obrazów w Google Search i Discover do standardowego rozmiaru. max-image-preview:large włącza podglądy pełnowymiarowe. W połączeniu z max-snippet:-1 (bez ograniczenia długości fragmentu tekstu) jest to domyślny robots meta na każdej stronie:
<meta name="robots" content="max-image-preview:large, max-snippet:-1">
Zaimplementowane jako domyślny {% block robots %} w base.html.twig. Szablony potomne nadpisują blok dla stron, które nie powinny być indeksowane — strony "coming soon" używają noindex, nofollow, strona wyszukiwania używa noindex.
Sitemap obrazów
Standardowy sitemap wyświetla URL stron. Sitemap obrazów dodaje bloki <image:image>, dając Google bezpośredni wgląd w lokalizacje obrazów i ich teksty alternatywne bez crawlowania każdej strony:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
<url>
<loc>https://holas.pl/wpis/</loc>
<image:image>
<image:loc>https://holas.pl/media/post-name/featured.webp</image:loc>
<image:title>Tekst alternatywny z frontmatter image_alt</image:title>
</image:image>
</url>
image:title pochodzi z pola frontmatter image_alt — tego samego tekstu, który jest używany w atrybucie HTML alt. Zarówno przestrzeń nazw xmlns:image, jak i blok image:image są w sitemap.xml.twig.
Co się zmieniło
Wynik Lighthouse SEO był 100 przed tymi zmianami. Po nich nadal jest 100. Ten wynik mierzy techniczne minimum: indeksowalność, tagi meta, responsywność mobilną.
Powyższe zmiany działają na innym poziomie. Dane strukturalne kształtują sposób reprezentowania treści przez wyszukiwarki w bogatych wynikach. Jawne meta społecznościowe zapewniają poprawne renderowanie bez polegania na logice fallback platformy. Rozszerzenia RSS pozwalają subskrybentom czytać pełne wpisy w swoim czytniku. Sitemap obrazów daje Google widoczność obrazów bez potrzeby crawlowania każdej strony.
Żadna z tych zmian nie jest architektonicznie złożona — większość to uzupełnienia szablonów Twig i deklaracje przestrzeni nazw. Ograniczeniem jest dyscyplina: każde pole wymaga rzeczywistej wartości z frontmatter, nie zastępnika.
Architektura, która sprawia, że to wszystko jest proste, opisana jest w części 2 tej serii — potok generowania statycznego, który produkuje kompletny HTML dla każdej strony.