På WordCamp Norge og WordUp Oslo er dette et tema som dukker opp hver gang noen bytter fra «standard kategoriwidget» til et skreddersydd arkiv. Spørsmålet er sjelden «finnes det en funksjon?» - det er «hvilken kategori skal styre stien når innlegget ligger i tre grener samtidig, og hvordan får vi Nynorsk-arkivtitler til å matche Bokmål-brødsmuler uten å hardkode strenger?».
WordPress lagrer hierarki i parent-feltet på term_taxonomy, men get_the_category() returnerer en flat tabell. Denne veiledningen går gjennom fellene, de innebygde hjelpefunksjonene, Primary Category fra Yoast og Rank Math, Schema.org BreadcrumbList, egendefinerte taksonomier og caching - med PHP du kan lime inn i et child theme.
Hvordan WordPress lagrer kategorihierarki
Kategorier er den innebygde hierarkiske taksonomien category. I motsetning til stikkord (post_tag) kan hvert term ha en overordnet (parent).
I databasen er dette ikke en kompleks graf. Hver rad i wp_term_taxonomy har et parent-felt med term_id til direkte forelder. Toppnivå (for eksempel «Teknologi») har parent = 0. Barnet «WordPress» peker på Teknologi, og barnebarnet «Gutenberg» peker på WordPress.
Det betyr at WordPress ikke lagrer en ferdig, flat sti i ett felt. Hvert steg oppover treet krever et oppslag i object cache eller database. Når du bygger brødsmuler, er jobben din å gå den stien deterministisk - ikke å håpe at indeks null i get_the_category() er riktig gren.
På norske redaksjonsnettsteder ser vi ofte tre nivåer: fagområde → undertema → format (for eksempel «Offentlig sektor» → «Digitalisering» → «Case»). Arkivmalen for /category/digitalisering/ må da vise både overordnet og gjeldende term. Hvis du bare printer $categories[0]->name, mister leseren konteksten som gjorde at de klikket inn fra forsiden eller et WordCamp Norge-sammendrag.
Et praktisk tips fra WordUp Oslo-sesjoner: tegn treet på en whiteboard før du koder. Redaktører tenker i «mapper», utviklere tenker i term_id. Når de to modellene ikke matcher, ender du med brødsmuler som peker til en søstergren fordi noen huket av feil avkrysningsboks i Gutenberg.
Fallgruver med get_the_category()
Inne i The Loop er det fristende å starte her:
$categories = get_the_category( $post->ID );Du får en tabell med WP_Term-objekter. To problemer dukker opp raskt:
- Ingen hierarkisk sortering. Har innlegget «Teknologi», «Programmering» og «PHP», sorterer WordPress vanligvis etter navn eller ID - ikke etter dybde i treet.
- Flere grener samtidig. En redaktør kan huke av både «PHP» og «Bransjenyheter». Hvilken skal styre stien over tittelen?
Naiv bruk av $categories[0] gir brødsmuler som hopper når alfabetet eller term_id endrer rekkefølgen. På flerspråklige norske nettsteder merkes det ekstra: Bokmål-etiketten «Nyheter» og Nynorsk-etiketten «Nyhende» kan bytte plass i sorteringen selv om strukturen er den samme.
// Unngå dette som eneste kilde til "gjeldende" kategori.
$maybe_primary = $categories[0] ?? null;Et annet fall: get_the_category() respekterer ikke orderby du kanskje forventer fra get_terms(). Den er en tynn wrapper rundt get_the_terms( $post_id, 'category' ). Hvis du trenger eksplisitt sortering (for eksempel etter dybde), må du gjøre det selv etter at tabellen er hentet.
Når innlegget ikke har noen kategori, returnerer funksjonen en tom tabell - ikke false. Sjekk alltid empty( $categories ) før du leser indeks. På eldre temaer som antar at «Uncategorized» alltid finnes, krasjer brødsmulen stille når redaksjonen har slettet standardkategorien og glemt å tilordne en ny.
Metode 1: get_category_parents()
WordPress har en hjelpefunksjon laget for akkurat denne jobben: get_category_parents(). Signaturen ser slik ut:
get_category_parents(
int $category_id,
bool $display_link = false,
string $separator = '/',
bool $nice_name = false,
array $deprecated = array()
): string|WP_ErrorEnkel bruk
function wppoland_vis_enkel_hierarki( int $post_id ): void {
$categories = get_the_category( $post_id );
if ( empty( $categories ) ) {
return;
}
$current = $categories[0];
$parents = get_category_parents(
$current->term_id,
true,
' <span class="sep">»</span> '
);
if ( is_wp_error( $parents ) ) {
return;
}
echo '<div class="breadcrumbs-simple">';
echo '<a href="' . esc_url( home_url( '/' ) ) . '">' . esc_html__( 'Forside', 'wppoland' ) . '</a>';
echo ' <span class="sep">»</span> ';
echo $parents; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- WP genererer anker.
echo '<span class="current-title">' . esc_html( get_the_title( $post_id ) ) . '</span>';
echo '</div>';
}Bruk esc_html__() (eller tilsvarende) for «Forside» slik at et Nynorsk-tema kan oversette etiketten uten å røre PHP-logikken. På WordUp Oslo-notater ser man ofte at arkivmalen er oversatt, mens brødsmulen fortsatt sier «Home» fordi noen hardkodet engelsk streng i header.php.
Begrensninger
get_category_parents() er praktisk, men:
- Den er låst til
category, ikke custom taxonomies. - HTML-en er en streng - vanskelig å cache per segment eller legge til
aria-current. - Den løser ikke Primary Category-konflikten. Du må fortsatt velge riktig
$category_idførst. - Med
$nice_name = truefår du slug i stedet for visningsnavn - nyttig for debug, sjelden for brukervendt UI.
Hvis du trenger å hoppe over toppnivået (noen design vil starte på barn, ikke «Forside » Teknologi»), er heller ikke denne funksjonen egnet. Da er get_ancestors() + egen filtrering av første ledd raskere enn å parse HTML-strengen med regex.
Metode 2: get_ancestors() for full kontroll
get_ancestors() er den mer fleksible veien. Den fungerer for alle hierarkiske taksonomier og returnerer en tabell med forfedre-ID-er (nærmeste forelder først).
function wppoland_bygg_kategori_sti( int $term_id ): array {
$ancestor_ids = get_ancestors( $term_id, 'category' );
$ancestor_ids = array_reverse( $ancestor_ids ); // rot → blad
$trail = array();
foreach ( $ancestor_ids as $ancestor_id ) {
$term = get_category( (int) $ancestor_id );
if ( $term && ! is_wp_error( $term ) ) {
$trail[] = $term;
}
}
$current = get_category( $term_id );
if ( $current && ! is_wp_error( $current ) ) {
$trail[] = $current;
}
return $trail;
}Render med semantisk HTML
function wppoland_render_breadcrumb_html( array $trail, int $post_id ): string {
if ( empty( $trail ) ) {
return '';
}
$html = '<nav class="breadcrumbs" aria-label="' . esc_attr__( 'Brødsmuler', 'wppoland' ) . '">';
$html .= '<ol class="breadcrumbs__list">';
$html .= '<li class="breadcrumbs__item">';
$html .= '<a href="' . esc_url( home_url( '/' ) ) . '">' . esc_html__( 'Forside', 'wppoland' ) . '</a>';
$html .= '</li>';
foreach ( $trail as $term ) {
$html .= '<li class="breadcrumbs__item">';
$html .= '<a href="' . esc_url( get_category_link( $term->term_id ) ) . '">';
$html .= esc_html( $term->name );
$html .= '</a></li>';
}
$html .= '<li class="breadcrumbs__item breadcrumbs__item--current" aria-current="page">';
$html .= esc_html( get_the_title( $post_id ) );
$html .= '</li>';
$html .= '</ol></nav>';
return $html;
}ol + aria-label er mer tilgjengelig enn en flat div med ». Skjermlesere får en liste, og du kan style separatoren med CSS ::before i stedet for hardkodede tegn.
Sett sammen Primary Category-valget og stien i én helper som single-malen kaller:
function wppoland_single_category_breadcrumb( int $post_id ): string {
$primary = wppoland_finn_primaer_kategori( $post_id );
if ( ! $primary ) {
return '';
}
$trail = wppoland_bygg_kategori_sti( (int) $primary->term_id );
$html = wppoland_render_breadcrumb_html( $trail, $post_id );
$html .= wppoland_breadcrumb_json_ld( $trail, $post_id );
return $html;
}Kall den fra single.php (eller via the_content-filter hvis du av en grunn ikke eier malen). Unngå å duplisere JSON-LD i både tema og SEO-plugin.
Primary Category: Yoast og Rank Math
Når innlegget har flere kategorier, må du velge én gren. SEO-plugins lagrer dette som post meta:
- Yoast SEO:
_yoast_wpseo_primary_category - Rank Math:
rank_math_primary_category
function wppoland_finn_primaer_kategori( int $post_id ): ?WP_Term {
$primary_id = (int) get_post_meta( $post_id, '_yoast_wpseo_primary_category', true );
if ( ! $primary_id ) {
$primary_id = (int) get_post_meta( $post_id, 'rank_math_primary_category', true );
}
if ( $primary_id ) {
$term = get_category( $primary_id );
if ( $term && ! is_wp_error( $term ) ) {
return $term;
}
}
// Fallback: dypeste node i treet.
$categories = get_the_category( $post_id );
if ( empty( $categories ) ) {
return null;
}
$deepest = null;
$max_depth = -1;
foreach ( $categories as $cat ) {
$depth = count( get_ancestors( $cat->term_id, 'category' ) );
if ( $depth > $max_depth ) {
$max_depth = $depth;
$deepest = $cat;
}
}
return $deepest;
}Dypeste node er et fornuftig fallback når Primary Category mangler: den speiler vanligvis den mest spesifikke redaksjonelle plasseringen. Hvis to kategorier har samme dybde, vinner den som kommer sist i løkken - dokumenter det for redaksjonen, eller sorter eksplisitt på term_id for stabilitet.
Yoast og Rank Math kan begge være aktive i migreringsperioder. Les Yoast først bare hvis det er prosjektets sannhetskilde; ellers bytt rekkefølge. Aldri bland: hvis Yoast har en Primary Category og Rank Math en annen, velg én plugin som eier feltet og deaktiver den andres breadcrumb-modul.
For redaktøropplæring: Primary Category er ikke «viktigste SEO-nøkkelord» i markedsføringsspråk - det er «hvilken gren i treet skal brødsmulen følge». Det skillet sparer timer med «hvorfor peker stien til Nyheter når artikkelen handler om PHP».
Schema.org BreadcrumbList i JSON-LD
Synlig HTML er ikke nok hvis du vil at Google skal forstå stien. Schema.org BreadcrumbList i JSON-LD er standardformatet.
HTML5-skisse
<nav class="breadcrumbs" aria-label="Brødsmuler">
<ol>
<li><a href="https://eksempel.no/">Forside</a></li>
<li><a href="https://eksempel.no/kategori/teknologi/">Teknologi</a></li>
<li><a href="https://eksempel.no/kategori/teknologi/wordpress/">WordPress</a></li>
<li aria-current="page">Slik bygger du brødsmuler</li>
</ol>
</nav>JSON-LD-generator
function wppoland_breadcrumb_json_ld( array $trail, int $post_id ): string {
$items = array();
$position = 1;
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => __( 'Forside', 'wppoland' ),
'item' => home_url( '/' ),
);
foreach ( $trail as $term ) {
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => $term->name,
'item' => get_category_link( $term->term_id ),
);
}
$items[] = array(
'@type' => 'ListItem',
'position' => $position,
'name' => get_the_title( $post_id ),
'item' => get_permalink( $post_id ),
);
$graph = array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $items,
);
return '<script type="application/ld+json">' .
wp_json_encode( $graph, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) .
'</script>';
}Hold HTML og JSON-LD i sync. Hvis den synlige stien hopper over «Forside», skal JSON-LD også gjøre det. Dobbel BreadcrumbList (tema + Yoast) gir konflikt - slå av den ene.
Valider med Googles Rich Results Test etter deploy. Typiske feil: position som starter på 0, manglende absolutt URL i item, eller name som er tom fordi get_the_title() kjørte utenfor loop. Bruk alltid eksplisitt $post_id.
På flerspråklige oppsett (Polylang / WPML): term-lenker må speile aktivt språk. get_category_link() respekterer vanligvis språkpluginens filtre, men hardkodede home_url( '/kategori/...' )-strenger gjør det ikke. Hold deg til API-ene.
Egendefinerte taksonomier
Mange norske innholdssider bruker register_taxonomy() for «tema», «region» eller «fagområde» i stedet for bare category. Da bytter du:
$ancestors = get_ancestors( $term_id, 'fagomrade' );
$term = get_term( $term_id, 'fagomrade' );
$link = get_term_link( $term );get_category_parents() finnes ikke for custom taxonomies. Bruk get_ancestors() + get_term() + get_term_link(), og sjekk is_wp_error() på lenken. Hierarkiske custom taxonomies oppfører seg som kategorier i parent-modellen, så Primary Category-mønsteret kan gjenskapes med egen post meta-nøkkel hvis redaksjonen trenger det.
Eksempel på en gjenbrukbar helper:
function wppoland_bygg_taksonomi_sti( int $term_id, string $taxonomy ): array {
if ( ! taxonomy_exists( $taxonomy ) ) {
return array();
}
$ancestor_ids = array_reverse( get_ancestors( $term_id, $taxonomy ) );
$trail = array();
foreach ( $ancestor_ids as $ancestor_id ) {
$term = get_term( (int) $ancestor_id, $taxonomy );
if ( $term && ! is_wp_error( $term ) ) {
$trail[] = $term;
}
}
$current = get_term( $term_id, $taxonomy );
if ( $current && ! is_wp_error( $current ) ) {
$trail[] = $current;
}
return $trail;
}Husk: hierarchical => false i register_taxonomy() betyr at parent alltid er 0. Da gir get_ancestors() en tom tabell, og brødsmulen blir bare ett ledd. Det er korrekt oppførsel - ikke en bug.
Ytelse og caching
På arkivsidene med mange innlegg koster gjentatte get_ancestors()-kall. Object cache (Redis/Memcached via Drop-in) hjelper fordi get_term() allerede er cachet, men du kan fortsatt memo-isere per request:
function wppoland_cached_trail( int $term_id ): array {
static $cache = array();
if ( isset( $cache[ $term_id ] ) ) {
return $cache[ $term_id ];
}
$cache[ $term_id ] = wppoland_bygg_kategori_sti( $term_id );
return $cache[ $term_id ];
}For fragment-cache (for eksempel i en full page cache foran PHP): cache nøkkelen bør inkludere post_id, Primary Category-ID og språk/locale. Et Bokmål-innlegg og et Nynorsk-arkiv som deler samme term_id men ulike etiketter via språkplugin, må ikke dele samme HTML-fragment.
Transient-cache av hele brødsmulestrengen er sjelden verdt det på enkeltinnlegg. Transient-er betaler seg når du bygger dyre, flernivå-menyer for hele treet - ikke for én sti på single.php.
Invalidér når term endres. Hvis du likevel cacher HTML, heng deg på edited_term og set_object_terms:
add_action( 'edited_category', 'wppoland_flush_breadcrumb_cache' );
add_action( 'set_object_terms', 'wppoland_flush_breadcrumb_cache' );
function wppoland_flush_breadcrumb_cache(): void {
// Slett gruppe-nøkler eller bump en cache-versjon i et options-felt.
delete_option( 'wppoland_breadcrumb_cache_ver' );
add_option( 'wppoland_breadcrumb_cache_ver', (string) time(), '', false );
}En versjonsbump i cache-nøkkelen er enklere enn å spore hver post_id som bruker et gitt term.
Vanlige spørsmål
Hvordan henter jeg gjeldende kategori?
Kall get_the_category( $post_id ). Velg deretter Primary Category eller den dypeste noden før du bygger stien. Ikke stol på indeks null alene.
Hva er forskjellen på get_category_parents() og get_ancestors()?
get_category_parents() returnerer en ferdig streng for category. get_ancestors() returnerer ID-er for alle hierarkiske taksonomier. Bruk sistnevnte når du eier markup, schema og caching selv.
Hva gjør jeg når flere kategorier ligger på samme nivå?
Sett Primary Category i Yoast eller Rank Math. Uten det: sorter deterministisk (for eksempel høyeste term_id) og dokumenter valget for redaksjonen, så stien ikke hopper mellom publiseringer.
Må BreadcrumbList inkludere innlegget?
Speil det brukeren ser. Hvis HTML ender på tittelen, skal JSON-LD gjøre det samme. Ikke legg til skjulte noder «for SEO».
Påvirker brødsmuler rangering?
De er primært UX og struktur. Korrekt BreadcrumbList kan gi rikere visning i søk. Dårlig synkronisering mellom HTML og JSON-LD gir støy, ikke magiske posisjoner.
Oppsummering og neste steg
- Ikke bruk
$categories[0]som eneste sannhet. - Foretrekk
get_ancestors()når du trenger kontroll; brukget_category_parents()for raske prototyper. - Les Primary Category-meta fra Yoast eller Rank Math, med dybde-fallback.
- Emitter
nav/olog matchende BreadcrumbList JSON-LD. - For custom taxonomies: samme mønster med
get_term/get_term_link.
Flernivå kategorihierarkier er der arkivmaler og brødsmuler sprekker først. Trenger du hjelp til å få hierarkiet riktig på tvers av tema og SEO-plugins, ta kontakt via WordPress-utvikling for arkiver og maler.






