21

Rozdział 21 z 21

SEO

Opublikowany

Domykamy blog SEO. Wygenerujemy sitemap XML z artykułami i ich tłumaczeniami, przypomnimy hreflang i canonical z Rozdziału 14 oraz dodamy meta tagi (description, Open Graph, Twitter Card) na stronie artykułu.

Czego się nauczysz

  • Sitemap XML dla artykułów i tłumaczeń
  • hreflang i canonical (z Rozdziału 14)
  • Meta tagi: title i description
  • Open Graph i Twitter Card

Rozdział 21: SEO

Ostatni rozdział. Domykamy blog od strony SEO: wygenerujemy sitemap XML (z artykułami i ich tłumaczeniami), przypomnimy hreflang i canonical z Rozdziału 14 oraz dodamy meta tagi (description, Open Graph, Twitter Card) na stronie artykułu.

Stan wejściowy: kompletny, wielojęzyczny blog. Tagi hreflang/canonical mamy już z Rozdziału 14 — tu dokładamy sitemap i meta tagi.


Sitemap XML

Wyszukiwarki lubią dostać listę wszystkich URL-i w formacie sitemap.xml. Generujemy go kontrolerem zwracającym XML — z artykułami i ich opublikowanymi tłumaczeniami:

// src/Controller/BlogSitemapController.php
#[Route('/sitemap.xml', name: 'app_blog_sitemap', defaults: ['_format' => 'xml'])]
public function sitemap(BlogArticleRepository $articles): Response
{
    $published = $articles->findBy(['status' => BlogArticle::STATUS_PUBLISHED], ['publishedAt' => 'DESC']);

    $response = $this->render('blog/sitemap.xml.twig', ['articles' => $published]);
    $response->headers->set('Content-Type', 'application/xml');

    return $response;
}

Szablon blog/sitemap.xml.twig wypisuje każdy artykuł (URL + data modyfikacji) oraz jego tłumaczenia jako <xhtml:link hreflang> — to sitemap świadomy wielu języków:

{% apply spaceless %}
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
    {% for article in articles %}
        <url>
            <loc>{{ url('app_blog_show', {slug: article.slug}) }}</loc>
            <lastmod>{{ article.updatedAt|date('Y-m-d') }}</lastmod>
            <xhtml:link rel="alternate" hreflang="pl" href="{{ url('app_blog_show', {slug: article.slug}) }}"/>
            {% for t in article.translations if t.status == 'published' and t.slug %}
                <xhtml:link rel="alternate" hreflang="{{ t.locale }}"
                            href="{{ url('app_blog_show', {slug: t.slug}) }}"/>
            {% endfor %}
        </url>
    {% endfor %}
</urlset>
{% endapply %}
  • /sitemap.xml — standardowa ścieżka; podajesz ją w Google Search Console i w robots.txt.
  • <lastmod> z updatedAt — mówi robotom, kiedy treść się zmieniła.
  • <xhtml:link hreflang> — te same wersje językowe co w <head>, ale w sitemapie (Google zaleca oba miejsca).

hreflang i canonical (przypomnienie)

Najważniejsze tagi SEO dla wielojęzyczności zbudowaliśmy już w Rozdziale 14 — w <head> strony artykułu:

{% set selfSlug = translation ? translation.slug : article.slug %}
<link rel="canonical" href="{{ url('app_blog_show', {slug: selfSlug}) }}">
<link rel="alternate" hreflang="pl" href="{{ url('app_blog_show', {slug: article.slug}) }}">
{% for t in article.translations if t.status == 'published' and t.slug %}
    <link rel="alternate" hreflang="{{ t.locale }}" href="{{ url('app_blog_show', {slug: t.slug}) }}">
{% endfor %}
<link rel="alternate" hreflang="x-default" href="{{ url('app_blog_show', {slug: article.slug}) }}">
  • canonical — wskazuje kanoniczny URL tej wersji językowej (bez duplikatów).
  • hreflang + x-default — mapa wersji językowych dla Google.

Meta tagi: title i description

Każda strona potrzebuje unikalnego <title> i meta description (to on często ląduje w wynikach wyszukiwania). Description budujemy z zajawki artykułu:

{# blog/show.html.twig — w bloku head #}
{% block title %}{{ displayTitle }}{% endblock %}

{% block metaDescription %}
    <meta name="description" content="{{ (displayExcerpt ?? displayContent)|striptags|slice(0, 160) }}">
{% endblock %}
  • |striptags — usuwa HTML/Markdown-owy narzut, zostawiając czysty tekst.
  • |slice(0, 160) — description ~160 znaków (tyle pokazuje Google).
  • displayExcerpt ?? displayContent — jeśli brak zajawki, bierzemy początek treści (z tłumaczenia lub oryginału — display* z Rozdziału 14).

Open Graph i Twitter Card

Żeby link do artykułu ładnie wyglądał po wklejeniu na Facebooka/LinkedIn/X, dodajemy tagi Open Graph i Twitter Card:

{# blog/show.html.twig — head #}
<meta property="og:type" content="article">
<meta property="og:title" content="{{ displayTitle }}">
<meta property="og:description" content="{{ (displayExcerpt ?? displayContent)|striptags|slice(0, 160) }}">
<meta property="og:url" content="{{ url('app_blog_show', {slug: selfSlug}) }}">
{% set ogImage = article.media|filter(m => m.type == 'image')|first %}
{% if ogImage %}
    <meta property="og:image" content="{{ absolute_url('/' ~ ogImage.path) }}">
{% endif %}
<meta name="twitter:card" content="summary_large_image">
  • og:image — bierzemy pierwszy obraz z galerii (BlogMedia, Rozdział 10); absolute_url() daje pełny URL (OG wymaga bezwzględnego).
  • twitter:card = summary_large_image — duży podgląd z obrazkiem na X.
  • OG i description korzystają z display* (Rozdział 14), więc każda wersja językowa ma własny podgląd.

robots.txt wskaż na sitemap: dodaj linię Sitemap: https://twoja-domena.pl/sitemap.xml, żeby roboty znalazły mapę.


Podsumowanie

Blog jest zoptymalizowany pod wyszukiwarki:

  • ✅ sitemap XML (/sitemap.xml) z artykułami, lastmod i hreflang dla tłumaczeń,
  • ✅ hreflang + canonical (z Rozdziału 14) — kanoniczne URL-e i mapa języków,
  • ✅ meta tagi — unikalny title i description (z zajawki/treści, striptags, ~160 znaków),
  • ✅ Open Graph + Twitter Card — ładny podgląd linku, og:image z galerii, per język,
  • ✅ działający stan: /sitemap.xml zwraca poprawny XML, a strona artykułu ma komplet tagów SEO.

To koniec 🎉

Przeszliśmy pełną drogę budowy bloga na Symfony 8 — w pięciu częściach, 21 rozdziałach:

  • I — Blog podstawowy: instalacja, fundament (User), encje, lista, artykuł, kategorie/tagi, komentarze, panel admina,
  • II — Bogata treść i media: Markdown z kolorowaniem kodu, obrazki, wideo/audio, upload, PDF,
  • III — Wielojęzyczność: tłumaczenia artykułów i kategorii, slug per język, generowanie AI,
  • IV — API: publiczne REST API z DTO i OpenAPI oraz API admina z tokenem i rate limitingiem,
  • V — Jakość i wdrożenie: testy jednostkowe i funkcjonalne, deployment i SEO.

Masz kompletny, wielojęzyczny, przetestowany i gotowy na produkcję blog. Powodzenia we własnych projektach!

Narzędzia / paczki

sitemap.xml hreflang Open Graph

Spis treści