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ęzycznego
  • wordCount — obliczany w czasie parsowania przez ContentItem::wordCount() (usuwa tagi HTML, liczy tokeny)
  • articleSection — kategoria wpisu
  • keywords — tagi wpisu jako ciąg rozdzielony przecinkami
  • image — zagnieżdżony ImageObject z url, width i height
{
    "@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.