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 pokazuję, 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.
Co zwraca get_the_category() i jak czytać obiekt
<?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].
Co robi get_the_category wewnątrz rdzenia WordPressa
W kodzie źródłowym wordpress-develop get_the_category() to cienka nakładka. Wywołuje get_the_terms( $post_id, 'category' ), zamienia wynik false albo WP_Error na pustą tablicę, przenumerowuje ją przez array_values(), przepuszcza każdy term przez prywatną funkcję _make_cat_compat() i zwraca wynik przez filtr get_the_categories. Z tych kilku linii wynikają trzy praktyczne konsekwencje.
Właściwości historyczne istnieją tylko w wyniku tej funkcji. _make_cat_compat() kopiuje nowe pola pod stare nazwy: cat_ID, cat_name, category_nicename, category_count, category_description i category_parent. Obiekty z get_the_terms() ich nie dostają. Szablon czytający $cat->cat_name działa dziś i przestaje cokolwiek wypisywać w dniu, w którym ktoś zamieni wywołanie na get_the_terms(), żeby obsłużyć własną taksonomię. Czytaj name, slug i term_id, wspólne dla obu funkcji.
Kolejność jest alfabetyczna, nie redakcyjna. get_the_terms() najpierw sprawdza cache powiązań termów posta, a gdy go brak, woła wp_get_object_terms(), które uruchamia WP_Term_Query z domyślnym orderby ustawionym na name. $categories[0] to więc pierwsza kategoria alfabetycznie, o ile żaden filtr nie zmieni kolejności.
Wtyczki mogą przepisać tablicę. Każdy callback podpięty pod get_the_categories (z argumentem $post_id od WordPressa 4.4) albo pod get_the_terms może dodać, usunąć lub przestawić termy, zanim zobaczy je szablon. Gdy badge pokazuje kategorię, której nikt nie przypisał, sprawdź callbacki na tych dwóch hookach, zanim zaczniesz debugować szablon.
Jak escapować nazwę kategorii z esc_html i esc_attr
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 piszę pod klientów (i które omawiam przy wdrożeniach custom theme), zakładają escapowanie przy każdym wyjściu do HTML.
Dlaczego nazwa kategorii pokazuje & poza HTML
Zanim term trafi do bazy, default-filters.php przepuszcza nazwę na hooku pre_term_name przez sanitize_text_field(), wp_filter_kses() i, z priorytetem 30, przez _wp_specialchars(). Tagi HTML znikają już przy zapisie, a kategoria „Porady & triki” leży w bazie jako Porady & triki.
W HTML to nie przeszkadza. esc_html() woła _wp_specialchars() z wyłączonym podwójnym kodowaniem, więc istniejące & zostaje &, a przeglądarka pokazuje zwykłe „&”. Dosłowne & wychodzi poza HTML: w JSON-LD budowanym przez wp_json_encode(), w polu REST, które JavaScript wpisuje przez textContent, w temacie maila tekstowego albo w eksporcie CSV. Bezpośrednie wywołanie htmlspecialchars() z PHP, które domyślnie koduje podwójnie, daje widoczne &amp;. W tych kontekstach najpierw dekoduj, potem stosuj escapowanie formatu docelowego:
<?php
$categories = get_the_category();
if ( ! empty( $categories ) ) {
// Zamień zapisane "&" z powrotem na "&" dla kontekstu innego niż HTML.
$nazwa = wp_specialchars_decode( $categories[0]->name, ENT_QUOTES );
$schema = array( 'articleSection' => $nazwa );
echo wp_json_encode( $schema );
}
?>Jak wypisać wiele kategorii przez 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.
Jak posortować i wykluczyć kategorie wpisu
Pętla z implode powyżej obsługuje podstawowy przypadek. W prawdziwych szablonach zwykle potrzebujesz dwóch rzeczy więcej: kontroli nad kolejnością i sposobu na pominięcie kategorii, których nigdy nie chcesz wypisywać (klasyczny przykład to “Bez kategorii” przeciekające do projektu karty).
Do kolejności wp_list_pluck() skraca pętlę do jednej linii, a zwykłe sort() daje porządek alfabetyczny. WordPress domyślnie zwraca kategorie posortowane po nazwie, ale wtyczki mogą to filtrować, więc jawne sortowanie nic nie kosztuje i usuwa jedną niespodziankę:
<?php
$categories = get_the_category();
if ( ! empty( $categories ) ) {
// Wyciągnij same nazwy z obiektów terminów
$names = wp_list_pluck( $categories, 'name' );
// Jawny porządek alfabetyczny, niezależny od filtrów
sort( $names );
echo esc_html( implode( ' / ', $names ) );
}
?>Żeby wykluczyć konkretne kategorie, porównuj slug, nie nazwę. Slugi są stabilne, a nazwy klienci zmieniają bez uprzedzenia:
<?php
$categories = get_the_category();
$skip = array( 'uncategorized', 'featured' );
$names = array();
foreach ( $categories as $category ) {
if ( in_array( $category->slug, $skip, true ) ) {
continue;
}
$names[] = $category->name;
}
if ( ! empty( $names ) ) {
echo esc_html( implode( ', ', $names ) );
}
?>Escapowanie dzieje się raz, na gotowym połączonym stringu. Escapowanie każdej nazwy osobno i dopiero potem implode też działa, ale jedno esc_html() w miejscu wypisania od razu pokazuje w code review, że nic nie opuszcza szablonu bez escapowania.
Dlaczego nie używać 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.
Jak pobrać nazwę z własnej taksonomii przez 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 widziałem 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
Jak pobrać nazwy kategorii jako tablicę przez wp_get_post_terms
wp_get_post_terms() przekazuje trzeci argument do wp_get_object_terms(), więc przyjmuje opcje WP_Term_Query, których get_the_terms() nie zna. Z 'fields' => 'names' zwraca tablicę stringów i pętla znika:
<?php
// Same nazwy, najpierw kategorie z największą liczbą wpisów.
$nazwy = wp_get_post_terms(
get_the_ID(),
'category',
array(
'fields' => 'names',
'orderby' => 'count',
'order' => 'DESC',
)
);
if ( ! is_wp_error( $nazwy ) && ! empty( $nazwy ) ) {
echo esc_html( implode( ', ', $nazwy ) );
}
?>Ceną jest cache. get_the_terms() najpierw czyta cache powiązań posta, a główne WP_Query wypełnia go dla wszystkich wpisów na stronie, bo update_post_term_cache ma domyślnie wartość true. wp_get_post_terms() omija ten cache i zawsze woła wp_get_object_terms(), więc na archiwum z dwudziestoma kartami robi dwadzieścia zapytań o termy, które WordPress już załadował. W pętli wp_list_pluck( get_the_category(), 'name' ) daje tę samą tablicę nazw z cache. wp_get_post_terms() zostaw na miejsca, gdzie potrzebujesz jego argumentów, na przykład sortowania po count w sidebarze albo callbacku REST obsługującego jeden wpis.
Jak odczytać 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.
Jak wyświetlić nazwę kategorii bez linku w FSE
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:
- Nie szukaj przełącznika w bloku Post Terms: jego block.json definiuje tylko atrybuty
term,separator,prefixisuffix, a render w PHP składa wynik przezget_the_term_list(). Każdy term jest tam linkiem. - Zamiast tego 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.
Jak cache’ować nazwy kategorii w WordPress
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 utrzymuję 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ł.
Jak pobrać nazwę kategorii na archiwum przez single_cat_title
W category.php kategoria nie należy do wpisu, tylko jest obiektem zapytania całej strony. Tam get_the_category() zwraca kategorie pierwszego wpisu w pętli, a nie kategorię archiwum. Właściwa funkcja to single_cat_title(); z false w drugim argumencie zwraca wartość zamiast ją wypisać:
<?php
if ( is_category() ) {
$nazwa = single_cat_title( '', false );
echo esc_html( $nazwa );
}
?>W general-template.php single_cat_title() przekazuje pracę do single_term_title(). Ta funkcja czyta get_queried_object(), na archiwach kategorii stosuje filtr single_cat_title i poza archiwami kategorii, tagów i taksonomii zwraca null. default-filters.php podpina pod ten filtr wptexturize i strip_tags; żadne z nich nie zastępuje escapowania, więc esc_html() zostaje na wyjściu. Jeśli potrzebujesz też slug albo term_id pod klasę CSS, czytaj obiekt bezpośrednio przez get_queried_object() i sprawdź, czy to instancja WP_Term.
Jak wstawić snippet nazwy kategorii do single.php
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.
Kiedy używać the_category z linkami
Niektóre miejsca potrzebują właśnie znaczników z linkami: klasyczna linia “Opublikowano w: Aktualności, Wydarzenia” pod pojedynczym wpisem, widget z metadanymi wpisu, w którym każda kategoria ma być klikalna, albo minimalistyczny motyw bez kart, w którym archiwa kategorii są nawigacją drugiego rzędu. Tam wywołaj the_category( ', ' ) albo użyj bloku Post Terms i nie odtwarzaj znaczników ręcznie. Jeśli projekt później zażąda linku wokół pojedynczej etykiety, owiń escapowaną nazwę w <a href="<?php echo esc_url( get_category_link( $term->term_id ) ); ?>">, zamiast wracać do the_category(). Błędem jest mieszanie obu trybów w jednym komponencie bez reguły, więc zapisz regułę w motywie: etykiety to tekst, linie metadanych to linki.
Dlaczego get_the_category zwraca puste albo złe kategorie
Brak wp_reset_postdata() we własnych pętlach. get_the_category() czyta globalne $post. We własnej pętli WP_Query, która wywołuje the_post(), zmienna globalna bez resetu dalej wskazuje ostatni element tej pętli, więc każde wywołanie kategorii po pętli zwraca kategorie niewłaściwego wpisu:
<?php
$related = new WP_Query( array( 'posts_per_page' => 3 ) );
while ( $related->have_posts() ) {
$related->the_post();
$cats = get_the_category();
// ... wypisanie ...
}
// Bez tej linii get_the_category() poniżej dotyczy ostatniego powiązanego wpisu
wp_reset_postdata();
?>Założenie, że wpis zawsze ma kategorię. Zwykłe wpisy dostają domyślnie “Bez kategorii”, ale własne typy treści z obsługą kategorii naprawdę mogą nie mieć żadnej, a kod, który na ślepo czyta $categories[0], rzuca wtedy notice o niezdefiniowanym indeksie. Warunek ! empty( $categories ) w przykładach tego artykułu jest tam celowo: zostaw go, nawet jeśli dziś “każdy wpis ma kategorię”.
Co sprawdzić przed wdrożeniem nazwy kategorii bez linku
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.






