How to display current and parent category of a post (WordPress snippet)

How to display current and parent category of a post (WordPress snippet)

Last verified: September 22, 2026
8 min read
Tutorial
Full-stack developer

Custom themes often need more than a single category name. Breadcrumbs, archive headers, and related-post filters all want the parent as well as the leaf term. WordPress stores categories as a hierarchy in the term_taxonomy table, but get_the_category() returns a flat array. You walk the parent property yourself.

This tutorial covers a one-level parent/child link, a full ancestor trail, primary-category selection with common SEO plugins, and optional BreadcrumbList JSON-LD. All output is escaped.

For broader theme and plugin work: WordPress developer.

#Why get_the_category() is not enough alone

get_the_category() returns WP_Term objects for the current post (or a given post ID). Each object has term_id, name, slug, and parent. The array is unordered with respect to hierarchy - a child can appear before its parent. If a post sits in Technology → WordPress → Performance, you may get three terms and still not know which one is the deepest leaf for a breadcrumb.

Practical rules:

  1. Decide which category is “primary” when several are assigned.
  2. Walk parent until it is 0 to build the trail for that primary term.
  3. Never echo names or URLs without esc_html() / esc_url().

#Method 1: current category plus immediate parent

Paste into a child theme functions.php or a small site-specific plugin:

function wppoland_get_category_hierarchy() {
	$categories = get_the_category();

	if ( empty( $categories ) ) {
		return false;
	}

	$category = $categories[0];
	$output   = '';

	if ( $category->parent ) {
		$parent = get_category( $category->parent );
		if ( $parent && ! is_wp_error( $parent ) ) {
			$output .= '<a href="' . esc_url( get_category_link( $parent->term_id ) ) . '">' . esc_html( $parent->name ) . '</a> &raquo; ';
		}
	}

	$output .= '<a href="' . esc_url( get_category_link( $category->term_id ) ) . '">' . esc_html( $category->name ) . '</a>';

	return $output;
}

#Usage in a template

<?php
$hierarchy = wppoland_get_category_hierarchy();
if ( $hierarchy ) {
	echo '<nav class="category-breadcrumb" aria-label="Category">' . $hierarchy . '</nav>';
}
?>

Example output: Technology » WordPress

This is enough for two-level trees. For deeper trees, use the full trail below.

#Method 2: full ancestor trail with a while loop

function wppoland_get_full_category_trail( $category_id ) {
	$trail = array();

	while ( $category_id ) {
		$category = get_category( $category_id );
		if ( ! $category || is_wp_error( $category ) ) {
			break;
		}
		array_unshift( $trail, $category );
		$category_id = (int) $category->parent;
	}

	$parts = array();
	foreach ( $trail as $cat ) {
		$parts[] = '<a href="' . esc_url( get_category_link( $cat->term_id ) ) . '">' . esc_html( $cat->name ) . '</a>';
	}

	return implode( ' &raquo; ', $parts );
}

Call it with the primary category ID:

$cats = get_the_category();
if ( ! empty( $cats ) ) {
	echo wppoland_get_full_category_trail( $cats[0]->term_id );
}

array_unshift keeps root → leaf order. The loop stops when parent is 0.

#Method 3: get_category_parents() for quick markup

Core already ships a helper:

$cats = get_the_category();
if ( ! empty( $cats ) ) {
	echo get_category_parents( $cats[0]->term_id, true, ' &raquo; ' );
}

Arguments: term ID, whether to link names, separator. Good for classic themes. Less flexible when you need Schema, custom classes, or to skip the leaf.

#Selecting a primary category

Editors often assign several categories. Breadcrumbs should follow one chain.

#Yoast SEO

function wppoland_get_primary_category( $post_id = null ) {
	$post_id = $post_id ?: get_the_ID();

	if ( class_exists( 'WPSEO_Primary_Term' ) ) {
		$primary = new WPSEO_Primary_Term( 'category', $post_id );
		$term_id = $primary->get_primary_term();
		if ( $term_id && ! is_wp_error( $term_id ) ) {
			$term = get_category( $term_id );
			if ( $term && ! is_wp_error( $term ) ) {
				return $term;
			}
		}
	}

	$categories = get_the_category( $post_id );
	return ! empty( $categories ) ? $categories[0] : null;
}

#Rank Math

function wppoland_get_rankmath_primary_category( $post_id = null ) {
	$post_id = $post_id ?: get_the_ID();
	$term_id = (int) get_post_meta( $post_id, 'rank_math_primary_category', true );

	if ( $term_id ) {
		$term = get_category( $term_id );
		if ( $term && ! is_wp_error( $term ) ) {
			return $term;
		}
	}

	$categories = get_the_category( $post_id );
	return ! empty( $categories ) ? $categories[0] : null;
}

Wire primary selection into the trail function so archives, related posts, and breadcrumbs agree on the same path.

Visible breadcrumbs help humans. Matching structured data helps search engines understand the path. Emit JSON-LD once in wp_head on singles:

function wppoland_schema_category_breadcrumbs() {
	if ( ! is_singular( 'post' ) ) {
		return;
	}

	$category = wppoland_get_primary_category();
	if ( ! $category ) {
		return;
	}

	$trail   = array();
	$current = $category;
	while ( $current ) {
		array_unshift( $trail, $current );
		$current = $current->parent ? get_category( $current->parent ) : null;
		if ( $current && is_wp_error( $current ) ) {
			$current = null;
		}
	}

	$items    = array();
	$position = 1;
	$items[]  = array(
		'@type'    => 'ListItem',
		'position' => $position++,
		'name'     => 'Home',
		'item'     => home_url( '/' ),
	);

	foreach ( $trail as $cat ) {
		$items[] = array(
			'@type'    => 'ListItem',
			'position' => $position++,
			'name'     => $cat->name,
			'item'     => get_category_link( $cat->term_id ),
		);
	}

	$items[] = array(
		'@type'    => 'ListItem',
		'position' => $position,
		'name'     => get_the_title(),
		'item'     => get_permalink(),
	);

	$schema = array(
		'@context'        => 'https://schema.org',
		'@type'           => 'BreadcrumbList',
		'itemListElement' => $items,
	);

	echo '<script type="application/ld+json">' . wp_json_encode( $schema ) . '</script>' . "\n";
}
add_action( 'wp_head', 'wppoland_schema_category_breadcrumbs' );

If your theme or SEO plugin already outputs BreadcrumbList, skip this to avoid duplicate graphs.

#Custom taxonomies note

Categories are one hierarchical taxonomy. For a custom taxonomy, use get_the_terms( $post_id, 'your_tax' ) and get_term_link(). Parent walking is the same via $term->parent. Do not call get_category() on non-category terms.

#Edge cases to test

  • Post with no categories - return early; do not print empty nav.
  • Orphaned parent ID (deleted parent) - get_category() can return an error; check is_wp_error().
  • Multisite / language plugins - archive links must respect the current site URL (get_category_link already does).
  • Block themes - prefer a shortcode or a small block that calls these helpers, rather than editing single.php only.

#Performance

Category lookups are cheap and object-cached in normal WordPress requests. Avoid calling the full trail builder dozens of times in a loop of posts; compute once per post or store a transient if you do heavy Schema work on archive pages.

#Working inside the Loop vs with an arbitrary post ID

All of the helpers above assume the global post is set (inside the Loop, or after setup_postdata). On widgets, REST callbacks, and CLI scripts, pass an explicit ID:

function wppoland_get_category_hierarchy_for_post( $post_id ) {
	$categories = get_the_category( $post_id );
	if ( empty( $categories ) ) {
		return false;
	}

	$primary = $categories[0];
	if ( function_exists( 'wppoland_get_primary_category' ) ) {
		$maybe = wppoland_get_primary_category( $post_id );
		if ( $maybe ) {
			$primary = $maybe;
		}
	}

	return wppoland_get_full_category_trail( $primary->term_id );
}

This matters on archive cards where you loop many posts: never call get_the_category() without the current card’s ID after the Loop advances.

#Styling and accessibility

Use a <nav> with an accessible name (aria-label="Category" or a translated string). Keep the visible separator (» or /) outside the link text so screen readers do not hear “Technology right-pointing double angle quotation mark WordPress” as one run-on phrase - putting separators between anchors (as in the implode version) is clearer.

Do not rely on color alone to show the current crumb. The last item can be plain text without a link if you are already on that archive; on single posts, linking the leaf category to its archive is usually what editors expect.

#Polylang and WPML

On multilingual sites, get_category_link() returns the URL for the term in the current language when the translation plugin is configured correctly. Still verify that primary-category meta from Yoast/Rank Math is per-language (or duplicated) after a translation workflow. A Polish primary term ID on an English post is a common migration bug.

#When not to build breadcrumbs from categories

If the site’s IA is driven by custom taxonomies, landing pages, or a manual breadcrumb plugin, do not also emit a second category trail. Duplicate BreadcrumbList JSON-LD and conflicting visible crumbs confuse both users and rich-result validators. Pick one source of truth per template.

#Summary

  1. get_the_category() gives terms; hierarchy lives in parent.
  2. One parent level: fetch $category->parent and link both.
  3. Full trail: walk parents with a while loop or use get_category_parents().
  4. Multiple categories: resolve primary via Yoast or Rank Math, then fall back.
  5. Escape every name and URL; optionally emit BreadcrumbList once.

Need this wired into a production theme or a block pattern? Contact WPPoland with the theme slug and whether you use classic or block templates.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Want this implemented on your site?

If you want to convert the article into a working site improvement, redesign, or build plan, I can define the scope and implement it.

Related cluster

Explore other WordPress services and knowledge base

Strengthen your business with professional technical support in key areas of the WordPress ecosystem.

How long does it take to display parent and current category in WordPress?#
About 10 to 15 minutes if you already know theme template files. Drop the snippet into single.php, a block theme pattern, or a shortcode wrapper.
What is the minimum WordPress version required for this snippet?#
Any modern WordPress release works. get_the_category(), get_category() and get_category_link() have been in core for years. Use PHP 7.4 or newer.
What can go wrong when displaying the category hierarchy?#
Assuming a post has only one category when WordPress allows many, and forgetting esc_html() or esc_url() on output. Both break markup or open XSS.
Should I use get_category_parents() instead of a custom loop?#
For simple HTML trails, yes - it is built in and fast. Use a custom loop when you need Schema.org markup, custom separators, or primary-category selection.
How do I pick a primary category when a post has several?#
Read Yoast WPSEO_Primary_Term or Rank Math post meta rank_math_primary_category. Fall back to the first term from get_the_category() if none is set.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles