Når du bygger egne WordPress-temaer for norske redaksjoner, trenger du ofte mer enn en flat kategoriliste. Brødsmuler (breadcrumbs) som viser både gjeldende kategori og dens overordnede gir leseren kontekst, og gir søkemotorer en tydelig sti gjennom innholdsklyngen.
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.
Hvorfor get_the_category() feiler for hierarki
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.
Hvordan bruke get_category_parents() til brødsmuler
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 av get_category_parents()
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 i get_category_parents()
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.
Hvordan bygge kategori-sti med get_ancestors()
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;
}Hvordan rendre brødsmulestien 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.
Hvordan håndtere Primary Category i 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».
Hvordan legge til 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-markup for brødsmuler
<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>PHP-funksjon som lager BreadcrumbList i JSON-LD
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.
Hvordan vise hierarki for 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.
Hvordan cache kategori-brødsmuler uten ytelsestap
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.
Oppsummering
- 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.







