Standardowa funkcja WordPressa the_category() jest wygodna na liście wpisów w klasycznym motywie, ale ma jedną sztywną cechę: zawsze generuje linki HTML (<a href="...">...</a>) do archiwum kategorii. Gdy budujesz kartę bloga, badge w gridzie portfolio albo meta w hero single, kategoria ma często być samym tekstem albo klasą CSS, a nie klikalnym elementem. W tym artykule pokazujemy, jak wyjść z kotwic i pracować na obiektach terminów.
Rozwiązaniem jest get_the_category(), która zwraca tablicę obiektów (w praktyce WP_Term), a nie gotowy markup. Oficjalna dokumentacja: get_the_category(), the_category() oraz get_the_terms().
Dlaczego the_category() zawsze wypisuje kotwice
the_category() to tag szablonu w sensie WordPressa: funkcja przeznaczona do wyświetlenia wyniku, nie do zwrócenia danych do dalszej obróbki. Wewnętrznie opiera się na get_the_category_list(), która składa listę linków z separatorem i opcjonalnym rodzicielskim kontekstem. Parametry sterują separatorem, rodzicami i post ID, ale nie trybem „tylko tekst”.
To nie jest bug. Architektura tagów szablonów z lat 2000 zakładała, że motyw wypisuje HTML od razu w pliku PHP. Dziś ten model koliduje z komponentami, block theme’ami i miejscami, gdzie nazwa trafia do atrybutu, JSON-LD albo cache’owanego meta. Gdy potrzebujesz stringa bez markup, nie walcz z the_category() - sięgnij po getter.
Na WordUpach w Trójmieście ten temat wraca regularnie: ktoś pokazuje „dlaczego badge jest niebieski i klikalny”, a odpowiedź jest zawsze ta sama - w szablonie siedzi the_category() zamiast odczytu ->name.
Kształt obiektu z get_the_category()
<?php
$categories = get_the_category();
if ( ! empty( $categories ) ) {
echo esc_html( $categories[0]->name );
}
?>get_the_category( $post_id = false ) bez argumentu bierze bieżący post z globalnego $post (w pętli). Zwracana tablica zawiera obiekty terminów taksonomii category. Pola, z których korzystasz na co dzień:
name- wyświetlana nazwa (to, czego zwykle chcesz bez linku)slug- bezpieczny identyfikator pod klasy CSS i ścieżkiterm_id- ID terminu w tabelitermsterm_taxonomy_id- ID wterm_taxonomy(rzadziej potrzebne w szablonach)description- opis kategorii z panelucount- liczba obiektów przypisanych do terminuparent- ID rodzica w hierarchii kategoriitaxonomy- zwykle stringcategory
Pusta tablica oznacza brak kategorii. Nie zakładaj, że każdy post ma przynajmniej jedną: importy, CPT z wyłączoną taksonomią albo ręczne czyszczenie danych potrafią zostawić pustkę. Zawsze ! empty( $categories ) przed indeksem [0].
Kolejność elementów w tablicy nie jest kontraktem „pierwsza = primary”. To kolejność zwrócona przez warstwę termów i cache obiektów. Jeśli SEO wymaga jednej „głównej” kategorii, czytaj meta wtyczki (sekcja niżej), a nie ślepo [0].
Escapowanie: esc_html, esc_attr i kontekst wyjścia
Nazwa kategorii pochodzi z bazy i może zawierać cudzysłowy, ampersandy albo znaki, które w HTML łamią atrybuty. Reguła jest prosta:
- treść między tagami:
esc_html( $term->name ) - wartość atrybutu (
class,data-*,aria-label):esc_attr( $term->slug )lubesc_attr( $term->name )zależnie od tego, co wstawiasz - URL archiwum (gdy jednak potrzebujesz linku):
esc_url( get_category_link( $term->term_id ) )
<?php
$cats = get_the_category();
$first = ! empty( $cats ) ? $cats[0] : null;
if ( $first ) : ?>
<span class="badge badge-<?php echo esc_attr( $first->slug ); ?>">
<?php echo esc_html( $first->name ); ?>
</span>
<?php endif; ?>Nie mieszaj: echo $category->name w motywie produkcyjnym to dług techniczny. Motywy, które piszemy pod klientów (i które omawiamy przy wdrożeniach custom theme), zakładają escapowanie przy każdym wyjściu do HTML.
Wiele kategorii: foreach i implode
Jeden wpis może mieć kilka kategorii. Lista tekstowa bez linków buduje się z tablicy nazw, nie z gotowego HTML:
<?php
$categories = get_the_category();
$names = array();
if ( ! empty( $categories ) ) {
foreach ( $categories as $category ) {
$names[] = esc_html( $category->name );
}
echo implode( ', ', $names );
}
?>Warianty warte rozważenia:
- separator lokalny: w PL często
,albo·; unikaj twardego bez powodu - limit: jeśli UI pokazuje tylko dwie etykiety,
array_slice( $categories, 0, 2 )przed pętlą - unikalność: przy dziwnych danych z migracji
array_unique( $names )chroni przed podwójnym badge
Gdy potrzebujesz jednocześnie nazwy i sluga (np. lista <li> z klasami), trzymaj obiekty dłużej i escapuj przy echo, zamiast wcześniej sklejać HTML w stringu.
Antywzorzec: strip_tags na liście kategorii
Spotykany skrót wygląda tak:
<?php
// Antywzorzec - nie kopiuj do produkcji
echo strip_tags( get_the_category_list( ', ' ) );
?>Dlaczego to zły pomysł:
- Generujesz pełny HTML z kotwicami, a potem go niszczysz - zbędna praca CPU i cache obiektów i tak już masz przy getterze.
strip_tagsnie zastępujeesc_html. Usuwa tagi, ale nie normalizuje encji w kontekście, w którym string potem ląduje.- Zmiana separatora, filtrów
the_category/get_the_category_listalbo HTML z wtyczek SEO potrafi zostawić śmieci w tekście. - Tracisz dostęp do
slugiterm_idw tym samym przebiegu - przy badge’ach i schema i tak wrócisz do obiektów.
Ten sam antywzorzec pojawia się przy the_tags() / get_the_tag_list(). Wzorzec naprawczy jest wspólny: get_the_terms() albo dedykowany getter, potem własne składanie stringa.
Własne taksonomie: get_the_terms()
Kategorie (category) i tagi (post_tag) to tylko dwie wbudowane taksonomie. Produkty WooCommerce, CPT portfolio, lokalizacje czy „branże” klientów siedzą we własnych rejestracjach. Tam get_the_category() milczy - nie dlatego, że „nic nie ma”, tylko dlatego, że patrzysz w złą taksonomię.
<?php
$post_id = get_the_ID();
$terms = get_the_terms( $post_id, 'branża' );
if ( is_wp_error( $terms ) || empty( $terms ) ) {
return;
}
$names = array();
foreach ( $terms as $term ) {
$names[] = esc_html( $term->name );
}
echo implode( ', ', $names );
?>get_the_terms() zwraca:
- tablicę
WP_Termprzy sukcesie false, gdy brak termówWP_Errorprzy problemie (np. niezarejestrowana taksonomia w danym kontekście)
Zawsze rozróżniaj is_wp_error() od pustki. W logach stagingu Trójmiasta widzieliśmy motywy, które traktowały WP_Error jak pustą tablicę i gubiły sygnał konfiguracyjny (taksonomia niezaładowana na froncie).
Pokrewne funkcje:
wp_get_post_terms()- więcej kontroli (pola, orderby), przydatne poza szablonemget_terms()- zapytanie po taksonomii bez konkretnego postahas_term()- warunek w layoutcie bez budowania listy
Primary category z wtyczek SEO
Yoast SEO, Rank Math i podobne wtyczki pozwalają redaktorowi wskazać „główną” kategorię przy wielu przypisaniach. Indeks [0] z get_the_category() tej decyzji nie zna.
Typowy odczyt (sprawdź aktualny klucz meta w swojej wersji wtyczki - nazwy bywały migracyjne):
<?php
$post_id = get_the_ID();
// Przykład wzorca Yoast - zweryfikuj klucz w swojej instalacji
$primary_id = (int) get_post_meta( $post_id, '_yoast_wpseo_primary_category', true );
if ( $primary_id > 0 ) {
$term = get_term( $primary_id, 'category' );
if ( $term && ! is_wp_error( $term ) ) {
echo esc_html( $term->name );
return;
}
}
// Fallback: pierwsza z get_the_category()
$categories = get_the_category( $post_id );
if ( ! empty( $categories ) ) {
echo esc_html( $categories[0]->name );
}
?>Rank Math i inne pakiety mają własne meta albo helpery API. W projekcie agencyjnym warto wyciągnąć ten odczyt do jednej funkcji motywu (wppoland_get_primary_category_name( $post_id )), żeby single, karty archiwum i feed RSS nie rozjeżdżały się logiką.
Bez primary meta decyzja biznesowa powinna być świadoma: alfabet, najpłytszy rodzic, albo kategoria z najwyższym count to trzy różne semantyki. Zapisz wybór w kodzie komentarzem, nie zgaduj przy code review.
Motywy blokowe i FSE: gdzie PHP jeszcze ma sens
W Full Site Editing nazwy kategorii często wychodzą z bloku Post Terms albo wzorców query loop. Blok domyślnie linkuje termy - to ten sam kontrakt UX co the_category(), tylko w HTML-u zapisanym w theme.json / markup bloku.
Gdy potrzebujesz tekstu bez linku w FSE:
- Sprawdź ustawienia bloku Post Terms (niektóre wersje pozwalają wyłączyć linki w inspectorze).
- Jeśli nie - użyj bloku Shortcode albo własnego bloku dynamicznego, który w
render_callbackwołaget_the_category()/get_the_terms(). - Alternatywa: filtruj output bloku przez
render_blocktylko dla konkretnegoblockNamei kontekstu szablonu - ostrożnie, bo łatwo zepsuć edytor.
Klasyczny single.php / content-card.php nadal jest najczytelniejszym miejscem na pełną kontrolę nad badge’ami. Hybrydy (FSE + klasyczne części szablonu) są normalne w utrzymywanych witrynach klientów; nie trzeba przepisywać całego motywu, żeby dostać esc_html( $term->name ) w jednym komponencie karty.
W block theme pamiętaj o kontekście zapytania: w niektórych callbackach get_the_ID() bywa puste, jeśli nie przekażesz postId z kontekstu bloku. Wtedy jawne $attributes['postId'] albo get_queried_object_id() ratuje odczyt.
Cache nazw kategorii pod meta i karty
Na archiwach i homepage’ach z gęstą siatką kart wielokrotne wołanie termów na post to zwykle koszt akceptowalny dzięki object cache WordPressa. Problemy zaczynają się, gdy:
- składasz własny REST / GraphQL / headless payload i serializujesz nazwy do JSON przy każdym requeście bez cache
- budujesz meta description albo Open Graph w pętli bez transientu przy ciężkim importcie
- generujesz programatycznie tysiące stron (np. lokalne landingi) i przy każdym URL powtarzasz te same
get_the_terms
Wzorzec, który utrzymujemy w projektach:
<?php
function wppoland_cached_category_names( $post_id ) {
$post_id = (int) $post_id;
$key = 'wpp_cat_names_' . $post_id;
$cached = wp_cache_get( $key, 'wppoland' );
if ( false !== $cached ) {
return $cached;
}
$categories = get_the_category( $post_id );
$names = array();
if ( ! empty( $categories ) ) {
foreach ( $categories as $category ) {
$names[] = $category->name; // escapuj przy wyjściu do HTML
}
}
wp_cache_set( $key, $names, 'wppoland', HOUR_IN_SECONDS );
return $names;
}
?>Invalidacja: podłącz set_object_terms / edited_term / deleted_term, żeby czyścić klucz przy zmianie przypisań. Trzymanie już zescapowanego HTML w cache utrudnia użycie tej samej listy w JSON-LD (tam zwykle chcesz surowy tekst i inne escapowanie). Cache’uj dane, escapuj przy renderze.
Do meta tagów (np. własny article:section) bierz primary name z poprzedniej sekcji, nie sklejaj pięciu kategorii w jeden string bez limitu długości - crawlerom i podglądom social i tak wystarczy jeden czytelny sygnał.
Gotowy snippet do single.php i kart
Minimalny wariant pierwszej kategorii:
<?php
$categories = get_the_category();
if ( ! empty( $categories ) ) {
echo esc_html( $categories[0]->name );
}
?>Lista po przecinku - jak wyżej w sekcji o implode. Badge ze slugiem - sekcja o escapowaniu. Primary z meta - sekcja SEO. Własna taksonomia - get_the_terms().
Ten sam kontrakt danych (WP_Term + esc_*) obowiązuje w shortcode’ach, widgetach klasycznych i render_callback bloków. Różni się tylko sposób przekazania $post_id.
Checklist wdrożeniowy
Zanim uznasz zadanie za zamknięte w motywie:
- Zero
the_category()/get_the_category_list()+strip_tagsw miejscach „tylko tekst”. - Każde wyjście nazwy przez
esc_htmllubesc_attrwedług kontekstu. - Jawna taksonomia przy CPT (
get_the_terms, nieget_the_category). - Primary category zgodna z wtyczką SEO, jeśli redakcja z niej korzysta.
- W FSE: decyzja blok vs PHP callback, bez ukrytego linkowania w Post Terms.
- Cache nazw tylko jako dane; invalidacja przy zmianie termów.
Jeśli szablon robi się nieutrzymywalny przez narosłe wyjątki kategorii, tagów i CPT, sensowniej jest uporządkować warstwę termów w motywie albo przepisać motyw od podstaw niż dokładać kolejne strip_tags.
Podsumowanie
the_category() zawsze buduje kotwice, bo tak został zaprojektowany tag szablonu. get_the_category() daje tablicę obiektów z name, slug, term_id i resztą metadanych - z tego składasz czysty tekst, badge albo meta. Własne taksonomie obsługuj przez get_the_terms(), primary category czytaj z meta wtyczki SEO, a strip_tags na listach HTML odłóż do historii. W block theme’ach kontroluj blok Post Terms albo własny render PHP. Nazwy pod meta i karty cache’uj jako dane, escapuj przy wyjściu.
Dokumentacja startowa na developer.wordpress.org: get_the_category, the_category, get_the_terms.







