The standard WordPress function the_category() is great, but it has one flaw: it always generates HTML links (<a href="...">...</a>) to the archive page. What if you’re building a custom layout (like a portfolio card or slider) where the category should be plain text, not a clickable element?
Learn more about professional WordPress development at WPPoland.
The solution is to use get_the_category(), which returns an array of objects instead of ready-made HTML.
Code (snippet)
Here’s a ready-to-use snippet you can paste into your single.php or content.php:
<?php
// Get all categories assigned to the current post
$categories = get_the_category();
if ( ! empty( $categories ) ) {
// Display the name of the first category found
echo esc_html( $categories[0]->name );
}
?>
Displaying a comma-separated list
If your post has multiple categories and you want to list them as comma-separated text:
<?php
$categories = get_the_category();
$output = array();
if ( ! empty( $categories ) ) {
foreach ( $categories as $category ) {
// Add name to the array
$output[] = esc_html( $category->name );
}
// Join the array into a string with a separator
echo implode( ', ', $output );
}
?>
Why get_the_category()?
This function gives you access to the full category object. Besides the name (->name), you can extract:
->slug(useful for CSS classes, e.g.,<span class="cat-<?php echo $cat->slug; ?>">)->term_id(category ID)->description(category description)->count(number of posts in this category)
Practical application
A common use case is styling labels on blog cards.
// Inside the WordPress loop
$cats = get_the_category();
$first_cat = !empty($cats) ? $cats[0] : null;
if ($first_cat) : ?>
<span class="badge badge-<?php echo esc_attr($first_cat->slug); ?>">
<?php echo esc_html($first_cat->name); ?>
</span>
<?php endif; ?>
This way, if you have a category “News”, you’ll get the class .badge-news, which you can easily color in CSS. This level of control is something the_category() simply doesn’t offer.
All the ways to get a category name as plain text
WordPress ships with several functions that hand you a category name without wrapping it in an anchor tag. Which one you pick depends on the context: a single post inside the loop, an archive page, or a spot where you only have a category ID.
get_the_category() with the first element. This is the approach from the snippet above and the best default inside the loop. It returns an array of WP_Term objects for the current post, so $categories[0]->name gives you the first name as a string, with the slug, ID and description one arrow away if you need them later.
get_cat_name() when you already have an ID. If the category ID is stored somewhere (a theme option, a custom field, a widget setting), you can skip the array entirely:
<?php
// Returns the name for category ID 12, or an empty string if it does not exist
echo esc_html( get_cat_name( 12 ) );
?>
single_cat_title() on archive pages. On a category archive (category.php), the category you care about is not attached to one post, it is the queried object of the whole page. get_the_category() is the wrong tool there. single_cat_title() reads the queried object directly, and passing false as the second argument makes it return the value instead of echoing it:
<?php
if ( is_category() ) {
$name = single_cat_title( '', false );
echo esc_html( $name );
}
?>
get_the_terms() vs get_the_term_list(). These two are confused constantly. get_the_term_list() builds ready-made HTML with links, which is exactly what this article is trying to avoid. get_the_terms() returns raw WP_Term objects, so it gives you the same control as get_the_category() and additionally accepts any taxonomy name:
<?php
// get_the_term_list() would print linked HTML. Use get_the_terms() instead:
$terms = get_the_terms( get_the_ID(), 'category' );
if ( $terms && ! is_wp_error( $terms ) ) {
echo esc_html( $terms[0]->name );
}
?>
Rule of thumb: reach for get_the_terms() when the code might switch to a custom taxonomy later, and get_the_category() when the template is category-only and you want the shortest call.
Working with multiple categories
The comma-separated loop earlier in the article covers the basic case. In real templates you usually need two more things: control over the order and a way to skip categories you never want printed (the classic example is “Uncategorized” leaking into a card design).
For ordering, wp_list_pluck() shortens the loop to one line and plain sort() handles alphabetical output. WordPress returns categories ordered by name by default, but plugins can filter that, so sorting explicitly costs nothing and removes a surprise:
<?php
$categories = get_the_category();
if ( ! empty( $categories ) ) {
// Pull just the names out of the term objects
$names = wp_list_pluck( $categories, 'name' );
// Explicit alphabetical order, independent of any filters
sort( $names );
echo esc_html( implode( ' / ', $names ) );
}
?>
To exclude specific categories, compare against the slug rather than the name. Slugs are stable, while names get edited by clients without warning:
<?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 ) );
}
?>
Note that escaping happens once, on the final joined string. Escaping each name and then imploding also works, but keeping a single esc_html() at the output point makes it obvious during code review that nothing leaves the template unescaped.
Custom taxonomies
Everything above extends to custom taxonomies with one function swap. get_the_terms() takes the post ID and the taxonomy slug, so the same pattern works for a portfolio “project type”, a WooCommerce product_cat, or any taxonomy registered by your theme:
<?php
$terms = get_the_terms( get_the_ID(), 'project_type' );
if ( $terms && ! is_wp_error( $terms ) ) {
$names = wp_list_pluck( $terms, 'name' );
echo esc_html( implode( ', ', $names ) );
}
?>
The is_wp_error() check is not optional decoration. get_the_terms() returns three different types: an array of terms on success, false when the post has no terms, and a WP_Error object when the taxonomy does not exist (a typo in the slug, or a plugin that registers the taxonomy being deactivated). A WP_Error is truthy, so an if ( $terms ) check alone lets it through and the foreach then throws a warning on every page load. The combined $terms && ! is_wp_error( $terms ) condition covers all three cases in one line.
Escaping and i18n
Every snippet here wraps output in esc_html(), and that habit matters more with category names than you might expect. Category names are user-entered content: an editor can create a category called Tips & Tricks or paste in a name containing angle brackets. Without escaping, the ampersand produces invalid HTML, and anything bracket-shaped becomes a live tag in your template.
There is one subtlety: WordPress stores some characters as entities. A category named Tips & Tricks may already sit in the database as Tips & Tricks. If you print it raw it renders fine by accident, but once you pass it through esc_html() you can end up with double encoding (&amp;) showing up literally on the page. When you see that symptom, decode first, then escape:
<?php
$categories = get_the_category();
if ( ! empty( $categories ) ) {
// Decode stored entities, then escape for output
echo esc_html( html_entity_decode( $categories[0]->name, ENT_QUOTES, 'UTF-8' ) );
}
?>
For multilingual sites, plain-text category output plays well with translation plugins because tools like Polylang and WPML filter the term query itself: get_the_category() and get_the_terms() already return the name for the current language, so no extra translation call is needed in the template.
Block editor and shortcode usage
Classic template tags do not help inside the block editor, patterns, or a page builder text widget. The cheapest bridge is a tiny shortcode that returns the plain category name, registered in functions.php or a small site plugin:
<?php
add_shortcode( 'plain_category', function () {
$categories = get_the_category();
if ( empty( $categories ) ) {
return '';
}
return esc_html( $categories[0]->name );
} );
?>
Drop [plain_category] into a Shortcode block, a synced pattern, or a query loop template and it prints the first category of the current post as text. Two details are easy to get wrong here. First, a shortcode callback must return its output, never echo it: echoing makes the text appear at the top of the content instead of where the shortcode sits. Second, inside a Query Loop block the global post is set per item, so the shortcode picks up the right category for each post in the loop without extra work.
Common mistakes
Reaching for the_category() and trying to strip the links afterwards. the_category() always prints linked markup, and there is no parameter to turn that off. Post-processing its output with strip_tags() technically works but is fragile and slower than asking for the raw data in the first place. If you need text, start from get_the_category() or get_the_terms().
Forgetting wp_reset_postdata() in custom loops. get_the_category() reads the global $post. In a custom WP_Query loop that calls the_post(), the global stays pointed at the last item of the custom loop unless you reset it, so any category call after the loop reports the wrong post’s categories:
<?php
$related = new WP_Query( array( 'posts_per_page' => 3 ) );
while ( $related->have_posts() ) {
$related->the_post();
$cats = get_the_category();
// ... output ...
}
// Without this, get_the_category() below refers to the last related post
wp_reset_postdata();
?>
Assuming a post always has a category. Regular posts get “Uncategorized” by default, but custom post types with category support can genuinely have none, and code that blindly reads $categories[0] then throws an undefined index notice. The ! empty( $categories ) guard used throughout this article is there for a reason: keep it even when “every post has a category” seems guaranteed today.






