Wskazówki SEO i GEO (AEO) dla projektów WordPress — bez wtyczek SEO
Szczegółowa analiza technik zastosowanych w motywie FSE BitBiz.pl — do przeniesienia na inne projekty WordPress. Wszystkie techniki działają bez wtyczek SEO (Yoast/RankMath), czysto w
functions.phpmotywu.
SEO = klasyczna optymalizacja pod Google. GEO / AEO (Generative Engine Optimization / Answer Engine Optimization) = optymalizacja pod systemy AI (ChatGPT Search, Perplexity, Google AI Overviews), które cytują treści i budują odpowiedzi na ich podstawie.
1. Dynamiczna meta description (bez wtyczki SEO)
Meta description to krótki opis strony w <head> HTML — Google
często pokazuje go jako szary tekst pod linkiem w wynikach wyszukiwania. Dobry opis nie podnosi
bezpośrednio pozycji, ale decyduje, czy ktoś kliknie. Hook wp_head to mechanizm WordPressa,
który pozwala "dokleić" własny kod do sekcji <head> każdej strony — a priorytet (liczba)
określa kolejność: im niższy, tym wcześniej kod się wykona.
Jeden hook wp_head z priorytetem 1 generuje description dla każdego typu strony:
| Typ strony | Źródło description |
|---|---|
Wpis / strona (is_singular) | Excerpt, a gdy pusty — pierwsze 20 słów treści (wp_trim_words) |
Kategoria / tag (is_category, is_tag) | term_description(), fallback: szablon „Artykuły powiązane z: {nazwa}" |
Strona autora (is_author) | Ręcznie napisany bio-opis z tytułem zawodowym i afiliacjami |
| Strona główna | get_bloginfo('description') |
add_action('wp_head', 'moj_seo_meta_description', 1);
function moj_seo_meta_description() {
if (is_singular()) {
global $post;
$excerpt = strip_tags($post->post_excerpt);
if (empty($excerpt)) {
$excerpt = strip_tags(wp_trim_words($post->post_content, 20, ''));
}
$excerpt = esc_attr(trim($excerpt));
if (!empty($excerpt)) {
echo '<meta name="description" content="' . $excerpt . '">' . "\n";
}
} elseif (is_category() || is_tag() || is_tax()) {
$desc = strip_tags(term_description());
if (empty($desc)) {
$term = get_queried_object();
$desc = 'Artykuły i aktualności powiązane z: ' . $term->name;
}
echo '<meta name="description" content="' . esc_attr(trim($desc)) . '">' . "\n";
} elseif (is_front_page() || is_home()) {
$desc = get_bloginfo('description', 'display');
if (!empty($desc)) {
echo '<meta name="description" content="' . esc_attr($desc) . '">' . "\n";
}
}
}
Zasady:
- Zawsze
esc_attr()+trim()+strip_tags(). - Fallback-chain: excerpt → treść → szablon generyczny. Nigdy pusta description.
- Priorytet
1nawp_head— meta description wysoko w<head>.
2. Open Graph + Twitter Cards
Tagi Open Graph (OG) decydują o tym, jak wygląda podgląd linku po wklejeniu go na Facebooka, LinkedIn, Slacka czy Discorda — tytuł, opis i obrazek w "kafelku". Bez nich serwisy społecznościowe zgadują (często źle), co pokazać. Twitter Cards to odpowiednik tego samego mechanizmu dla X/Twittera.
Drugi hook (wp_head, priorytet 2) generuje pełny zestaw OG dla każdego typu strony:
og:site_name,og:title,og:description,og:url,og:type,og:imagetwitter:card=summary_large_image+twitter:title/description/image
Kluczowe decyzje:
og:typezależny od kontekstu:articledla wpisów,profiledla strony autora,websitedla reszty. To sygnał typologiczny również dla crawlerów AI.- Fallback obrazka:
get_site_icon_url() ?: get_template_directory_uri() . '/favicon.png'— każda strona MA og:image, nawet bez obrazka wyróżniającego. - Obrazek wyróżniający w pełnym rozmiarze:
get_the_post_thumbnail_url($post->ID, 'full'). - Description współdzielona z meta description (ta sama logika excerpt → treść).
3. Schema.org JSON-LD — serce SEO i GEO
Schema.org to słownik ustrukturyzowanych danych, a JSON-LD to
sposób ich zapisu — niewidoczny dla użytkownika blok <script type="application/ld+json">
w kodzie strony, w którym mówimy maszynom wprost: "to jest artykuł, napisał go ten człowiek,
opublikowano go tego dnia". Google używa tych danych do rich results (gwiazdki, breadcrumbs,
daty w wynikach), a systemy AI — do rozumienia, kto i o czym pisze. "Encja" w tym kontekście
to po prostu konkretny, jednoznaczny obiekt w bazie wiedzy: ta osoba, ta firma, ten artykuł.
Generowane ręcznie w PHP (json_encode z flagami JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
hook wp_head priorytet 5. Typy schema per widok:
| Widok | Schema |
|---|---|
| Każda strona | Organization (z founder, address, contactPoint, sameAs) |
| Strona główna | WebSite + SearchAction (sitelinks searchbox) |
| Wpis | NewsArticle + BreadcrumbList |
| Strona autora | ProfilePage z mainEntity = Person |
| Kategoria / tag | CollectionPage + BreadcrumbList |
3.1. Konsolidacja encji Person przez @id (najważniejszy trik E-E-A-T)
Autor pojawia się w schema w kilku miejscach (jako autor artykułu, założyciel
organizacji, właściciel strony profilowej). Bez @id Google widzi trzy osobne wzmianki
o kimś o tym samym nazwisku i nie ma pewności, że to ta sama osoba. Nadanie węzłowi Person
stałego identyfikatora @id i odwoływanie się do niego wszędzie skleja te wzmianki w jedną
encję — a im mocniejsza encja autora, tym wyższy E-E-A-T, czyli ocena Google'a: czy autor
ma doświadczenie (Experience), ekspertyzę (Expertise), autorytet (Authoritativeness)
i wiarygodność (Trust) w temacie, o którym pisze.
Jedna funkcja zwraca pełny węzeł Person z ustalonym @id:
function moj_get_author_person_schema() {
$author_url = home_url('/author/nazwa-autora/');
return array(
'@type' => 'Person',
'@id' => $author_url . '#person', // ← stały identyfikator węzła
'name' => 'Imię Nazwisko',
'jobTitle' => 'Tytuł zawodowy',
'url' => $author_url,
'image' => get_template_directory_uri() . '/assets/autor.jpg',
'description' => 'Bio z doświadczeniem, afiliacjami, specjalizacją…',
'worksFor' => array('@type' => 'Organization', 'name' => '…', 'url' => '…'),
'memberOf' => array('@type' => 'Organization', 'name' => '…', 'url' => '…'),
'knowsAbout' => array('temat 1', 'temat 2', '…'), // ← mapa ekspertyz dla AI
'sameAs' => array('https://linkedin.com/…', 'https://github.com/…'),
);
}
Ten sam węzeł jest potem spinany referencjami w jeden graf wiedzy:
Organization.founder→ pełny węzeł Person (drukowany na każdej stronie),NewsArticle.author→ tylko referencjaarray('@id' => $author_url . '#person'),ProfilePage.mainEntity→ pełny węzeł Person.
Dzięki temu Google i systemy AI widzą jedną encję autora, a nie trzy rozproszone. To bezpośrednio wzmacnia E-E-A-T (Experience, Expertise, Authoritativeness, Trust).
3.2. NewsArticle — pola ważne dla GEO
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'NewsArticle', // lub 'Article'/'BlogPosting' zależnie od serwisu
'mainEntityOfPage' => array('@type' => 'WebPage', '@id' => get_permalink()),
'headline' => get_the_title(),
'description' => $description,
'inLanguage' => 'pl-PL', // ← jawny język treści
'datePublished' => get_the_time('c'),
'dateModified' => get_the_modified_time('c'), // ← świeżość, sygnał dla AI
'wordCount' => str_word_count(strip_tags($post->post_content)),
'author' => $author_schema, // referencja @id — patrz 3.1
'publisher' => array('@type' => 'Organization', 'name' => …, 'logo' => …),
'keywords' => $keywords, // ← z tagów wpisu: implode(', ', tag names)
// speakable — asystenci głosowi i AI wybierają kluczowe fragmenty
'speakable' => array(
'@type' => 'SpeakableSpecification',
'cssSelector' => array('h1', 'h2', '.entry-content > p:first-of-type'),
),
);
Uwagi:
speakableto sygnał czysto GEO/AEO — wskazuje AI, które selektory zawierają esencję artykułu.keywordsgenerowane automatycznie z tagów WP — zero ręcznej pracy redakcji.wordCount— sygnał głębokości treści.
3.3. BreadcrumbList
Dla wpisu: Strona główna → Kategoria → Artykuł (pozycje 1-2-3, pierwsza kategoria wpisu).
Dla kategorii/tagu: Strona główna → Termin. Drukowany jako osobny blok <script type="application/ld+json">.
3.4. ProfilePage (strona autora)
Typ wprowadzony przez Google specjalnie do walidacji encji twórców:
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'ProfilePage',
'dateModified' => get_lastpostmodified('server'),
'mainEntity' => moj_get_author_person_schema(),
);
3.5. Organization — sameAs i dane adresowe
sameAs (LinkedIn, GitHub, strona firmowa) wzmacnia Brand Mention Rate — AI kojarzy markę
z profilami zewnętrznymi. address (PostalAddress) i contactPoint dodają sygnał realnego
podmiotu (trust).
4. E-E-A-T w warstwie treści (nie tylko schema)
Schema to za mało — encja autora musi istnieć też w widocznym HTML:
- Dedykowany szablon strony autora (
templates/author-{nicename}.html) z sekcją hero: zdjęcie, H1 z imieniem i nazwiskiem, tytuł zawodowy, 2 akapity bio (doświadczenie, afiliacje, specjalizacje), linki social (rel="me"na link do własnej strony). - Boks autora pod każdym artykułem — zdjęcie, nazwisko linkujące do strony autora,
tytuł zawodowy, krótkie bio, link „Wszystkie artykuły autora →". Rejestrowany jako
register_block_pattern()w PHP (bo URL zdjęcia różni się między środowiskami dev/prod). - Tytuł strony autora celowany w zapytanie o nazwisko (encja Person):
add_filter('document_title_parts', function ($parts) {
if (is_author()) {
$author = get_queried_object();
if ($author && $author->user_nicename === 'nazwa-autora') {
$parts['title'] = 'Imię Nazwisko — Tytuł Zawodowy';
}
}
return $parts;
});
- Spójność encji wszędzie: to samo imię i nazwisko, tytuł zawodowy, afiliacje i linki w: schema Person, meta description strony autora, hero, boksie autora i llms.txt. Powtarzalność = konsolidacja encji w grafach wiedzy AI.
5. GEO: wirtualny endpoint /llms.txt
Plik llms.txt to rodzący się standard informowania systemów AI o zawartości serwisu i zasadach
cytowania. Serwowany wirtualnie (bez fizycznego pliku) przez hook init:
add_action('init', 'moj_serve_llms_txt', 1);
function moj_serve_llms_txt() {
$path = rtrim((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
$home_path = rtrim((string) parse_url(home_url(), PHP_URL_PATH), '/');
if ($path !== $home_path . '/llms.txt') return;
header('Content-Type: text/plain; charset=utf-8');
echo "# " . get_bloginfo('name') . "\n\n";
echo "> " . get_bloginfo('description') . "\n";
echo "> Wydawca i redaktor: Imię Nazwisko — tytuł (email)\n\n";
echo "## Treści dostępne do cytowania\n\n";
echo "Dozwolone: wyszukiwanie, cytowanie, synteza odpowiedzi.\n";
echo "Niedozwolone: trening modeli bez zgody właściciela.\n\n";
echo "## Sekcje\n\n";
echo "- Artykuły: " . home_url('/') . "\n";
echo "- Profil autora: " . home_url('/author/…/') . "\n";
echo "- Kontakt: " . home_url('/kontakt/') . "\n\n";
echo "## Autor i właściciel\n\n";
echo "Bio autora z afiliacjami i kontaktem…\n";
exit;
}
Zawartość llms.txt: nazwa serwisu, opis, kto wydaje, polityka cytowania (dozwolone/niedozwolone), mapa kluczowych sekcji, bio autora z linkami. To wizytówka serwisu dla LLM-ów.
6. GEO: selektywny robots.txt — boty wyszukujące vs trenujące
Kluczowe rozróżnienie strategiczne: boty AI-search cytują i odsyłają ruch (wpuszczamy),
boty treningowe konsumują treść bez zwrotu (blokujemy). Filtr robots_txt:
add_filter('robots_txt', function ($output, $public) {
if (!$public) return $output;
$output .= "\n# AI Search Bots — cytują treści i generują ruch (dozwolone)\n";
$output .= "User-agent: OAI-SearchBot\nAllow: /\n\n"; // ChatGPT Search
$output .= "User-agent: PerplexityBot\nAllow: /\n\n"; // Perplexity
$output .= "User-agent: GoogleOther\nAllow: /\n\n"; // Google AI
$output .= "# AI Training Bots — trening bez zwrotu ruchu (blokowane)\n";
$output .= "User-agent: GPTBot\nDisallow: /\n\n"; // trening OpenAI
$output .= "User-agent: CCBot\nDisallow: /\n\n"; // Common Crawl
$output .= "User-agent: anthropic-ai\nDisallow: /\n\n"; // trening Anthropic
$output .= "User-agent: Omgilibot\nDisallow: /\n\n";
return $output;
}, 10, 2);
Spójne z deklaracją w llms.txt („trening bez zgody niedozwolony").
7. Google Indexing API — automatyczny ping przy publikacji
Cały przepływ w skrócie: publikujesz wpis → WordPress w tle (nie blokując zapisu) wysyła do Google sygnał "mam nowy URL, wejdź i zaindeksuj" → Google odwiedza stronę w ciągu minut zamiast dni czekania na naturalny crawl. Service account to "konto techniczne" Google Cloud, którym serwer uwierzytelnia się w API bez udziału człowieka, a WP-Cron to wbudowany w WordPressa mechanizm zadań w tle.
Architektura:
- Trigger:
transition_post_status— tylko przy pierwszej publikacji ($new_status === 'publish' && $old_status !== 'publish'), żeby nie spamować API przy każdej edycji literówki. - Asynchronicznie przez WP-Cron:
wp_schedule_single_event(time(), 'moj_async_google_ping', [$post_ID])— publikacja wpisu nie czeka na odpowiedź API Google. - Autoryzacja: service account JSON (
WP_CONTENT_DIR . '/service_account.json'), ręczne JWT RS256 (openssl_sign) → token OAuth → POST nahttps://indexing.googleapis.com/v3/urlNotifications:publishztype: URL_UPDATED. - Logowanie: każdy ping (URL, status HTTP, body odpowiedzi) do
WP_CONTENT_DIR . '/google_indexing.log'— diagnostyka bez debuggera.
Wymagania: projekt w Google Cloud z włączonym Indexing API, service account dodany jako właściciel w Search Console. Plik klucza NIGDY w repo — wgrywany osobno na serwer.
8. Techniczne SEO — wydajność i struktura
8.1. Semantyczny HTML w szablonach FSE
Bloki grupy z jawnym tagName: <main>, <article>, <header>, <aside> (sidebar),
<footer>. Jeden <h1> na stronę (post-title level 1). Struktura czytelna dla parserów AI.
8.2. Optymalizacja zapytań (crawl budget + UX)
add_action('pre_get_posts', function ($query) {
if (!is_admin() && $query->is_main_query()) {
if ($query->is_search()) {
$query->set('post_type', 'post'); // szukaj tylko wpisów
$query->set('posts_per_page', 20);
}
if ($query->is_archive()) {
$query->set('posts_per_page', 20);
}
}
});
8.3. Cache w .htaccess (Lighthouse 100/100)
- Obrazy (jpg/png/webp/avif/svg) i fonty:
Cache-Control: public, max-age=31536000(fonty dodatkowoimmutable), - CSS/JS: 1 tydzień (
max-age=604800), Options -Indexes(bez listowania katalogów),ExpiresByTypeprzezmod_expiresjako uzupełnienie.
8.4. Core Web Vitals
Core Web Vitals to trzy metryki, którymi Google mierzy odczuwalną szybkość strony i które wprost wpływają na ranking. LCP (Largest Contentful Paint) — jak szybko pojawia się największy element widoczny na starcie, zwykle zdjęcie hero. CLS (Cumulative Layout Shift) — czy treść "skacze" podczas ładowania. FOIT (Flash of Invisible Text) — moment, w którym tekst jest niewidoczny, bo czeka na doładowanie webfonta.
- LCP: zdjęcie hero z
loading="eager" fetchpriority="high"; pozostałe obrazyloading="lazy"z jawnymiwidth/height(zapobiega CLS). - CLS: rezerwowanie przestrzeni pod dynamiczną treść;
scroll-padding-topkompensujący sticky header. - FOIT:
font-display: swap. - Above the fold: redukcja paddingów, żeby treść nie spadała pod linię zgięcia.
- Obrazy wyróżniające w proporcji
16/9(spójny layout, brak przeskoków).
9. Kolejność hooków wp_head (istotna!)
| Priorytet | Co |
|---|---|
| 1 | meta description |
| 2 | Open Graph + Twitter Cards |
| 5 | Schema.org JSON-LD |
Meta najważniejsze dla snippetów wyżej w <head>, schema po nich.
10. Uzupełnienia z audytu projektu Next.js: canonical, hreflang, sitemap, local SEO
Techniki, które wyszły przy audycie bliźniaczego projektu multi-site w Next.js — uniwersalne, choć w WordPressie część załatwia core.
10.1. Canonical URL
Tag <link rel="canonical"> mówi Google, która wersja adresu jest "tą właściwą" — bez niego
parametry URL, paginacja czy wersje z/bez ukośnika potrafią rozmyć ranking między duplikaty.
WordPress od wersji 2.9 dodaje canonical sam, ale tylko na stronach singular — archiwa,
strony paginowane i wyniki wyszukiwania trzeba obsłużyć samodzielnie (hook wp_head)
albo świadomie zostawić z noindex.
10.2. Hreflang — wersje językowe
Przy więcej niż jednym języku każda strona musi linkować wszystkie swoje wersje językowe
(w obie strony!) plus x-default:
<link rel="alternate" hreflang="pl" href="https://example.pl/oferta/">
<link rel="alternate" hreflang="en" href="https://example.pl/en/offer/">
<link rel="alternate" hreflang="x-default" href="https://example.pl/">
Bez tego Google potrafi pokazać polską stronę anglojęzycznemu użytkownikowi. Pamiętaj też
o atrybucie lang na <html> — musi zgadzać się z językiem treści na każdej stronie,
także w podkatalogu /en/.
10.3. Sitemap — i pułapka lastmod
WordPress od 5.5 generuje /wp-sitemap.xml z core'a. Jedna zasada, której łatwo nie doceniać:
pole lastmod musi być prawdziwe. Ustawianie bieżącej daty (np. daty builda przy stronach
statycznych) na wszystkich URL-ach naraz sprawia, że Google po kilku cyklach przestaje ufać
temu polu dla całej domeny — lepiej nie podawać lastmod wcale, niż podawać fałszywy.
10.4. Meta robots: max-snippet i max-image-preview
add_filter('wp_robots', function ($robots) {
$robots['max-image-preview'] = 'large';
$robots['max-snippet'] = -1;
$robots['max-video-preview'] = -1;
return $robots;
});
max-image-preview:large pozwala Google pokazywać duże miniatury (Discover!), a max-snippet:-1
zdejmuje limit długości cytowanego fragmentu — co dotyczy także AI Overviews: zbyt niski
limit snippetu ogranicza, ile AI może zacytować z Twojej strony. To sygnał GEO, nie tylko SEO.
10.5. Local SEO — schema dla firm usługowych
Dla biznesu z lokalnym zasięgiem Organization warto zastąpić typem LocalBusiness lub
konkretniejszym (ProfessionalService, ArtGallery…) i dodać pola, których "zwykłe" SEO
nie potrzebuje:
geo(GeoCoordinates) +areaServed(lista City/AdministrativeArea) — zasięg działania,openingHoursSpecification,priceRange,telephone— dane do panelu wizytówki,- katalog usług jako osobne węzły
Servicezprovider: {"@id": "...#organization"}— ta sama konsolidacja przez@id, co przy encji Person (sekcja 3.1).
Ostrzeżenie: nie dodawaj aggregateRating ani review o własnej firmie na własnej
stronie — Google od 2019 traktuje to jako self-serving reviews: markup jest ignorowany,
a przy nadużyciu grozi ręczna kara. Opinie w markup tylko wtedy, gdy dotyczą innych
podmiotów (np. recenzje produktów, które sprzedajesz).
11. Checklist wdrożenia w nowym projekcie WordPress
Fundamenty SEO:
- Dynamiczna meta description z fallback-chain (excerpt → treść → szablon)
- Open Graph + Twitter Cards z fallbackiem og:image (site icon → favicon motywu)
-
document_title_partsdla stron specjalnych (autor, landing) - Semantyczne tagi w szablonach (
main,article,header,aside), jeden H1 - Canonical na archiwach/paginacji (singular załatwia WP core)
- Hreflang + poprawny
langna<html>przy wersjach językowych -
wp_robots:max-image-preview:large,max-snippet:-1
Schema.org (JSON-LD):
-
Organizationna każdej stronie (sameAs, address, contactPoint, founder) -
WebSite+SearchActionna stronie głównej -
NewsArticle/Articlez datePublished/dateModified/inLanguage/wordCount/keywords/speakable -
BreadcrumbList(wpis: Home → Kategoria → Artykuł; termin: Home → Termin) -
ProfilePage+ centralna encjaPersonz@id, referencjonowana w author/founder/mainEntity -
CollectionPagedla kategorii i tagów - Firmy lokalne:
LocalBusiness/ProfessionalServicez geo, areaServed, openingHours, priceRange + usługi jakoServicez provider@id(bez self-serving reviews!)
E-E-A-T:
- Dedykowany szablon strony autora z hero (zdjęcie, bio, afiliacje, social z
rel="me") - Boks autora pod każdym artykułem (block pattern rejestrowany w PHP)
-
knowsAbout+memberOf+worksForw schema Person - Spójna encja autora we wszystkich miejscach (schema, meta, HTML, llms.txt)
- Strony „O nas" i „Kontakt" (trust signals)
GEO / AEO:
- Wirtualny endpoint
/llms.txt(opis serwisu, polityka cytowania, sekcje, autor) -
robots.txt: Allow dla botów AI-search (OAI-SearchBot, PerplexityBot, GoogleOther), Disallow dla botów treningowych (GPTBot, CCBot, anthropic-ai, Omgilibot) -
speakablew schema artykułów -
sameAsw Organization (Brand Mention Rate)
Indeksacja i wydajność:
- Google Indexing API: ping przy pierwszej publikacji, async przez WP-Cron, z logiem
- Sitemap z prawdziwym
lastmod(albo bez tego pola — nigdy z datą builda) - Cache headers w .htaccess (obrazy/fonty 1 rok, CSS/JS 1 tydzień)
- LCP:
fetchpriority="high"na hero,loading="lazy"+ wymiary na reszcie -
font-display: swap,scroll-padding-toppod sticky header - Limit wyników wyszukiwania/archiwów przez
pre_get_posts
12. Zasady bezpieczeństwa przy generowaniu meta/schema
- Zawsze
esc_attr()/esc_url()przy echo do HTML. json_encode(..., JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)— czytelne polskie znaki i URL-e w JSON-LD.- Klucze API (service account) poza repo, w
wp-content/, z walidacjąfile_exists(). - Generuj własne meta tylko gdy nie ma wtyczki SEO — inaczej duplikaty tagów.