Right, so you’ve gone to all the trouble of meticulously categorizing your content. You’ve got your ‘Genre’ taxonomy for your movie reviews and your ‘Ingredients’ taxonomy for your recipes. Pat yourself on the back. But a taxonomy sitting alone in the admin panel is like a meticulously organized toolbox you never open. It’s useless. The real magic, the reason we bother with this whole taxonomy rigmarole, is to dynamically connect content for the person actually reading your site. Showing a user “Oh, you liked Die Hard? Here are five other 80s Action movies we’ve reviewed” is the entire point. Let’s get that magic on the screen.

We’ll be using three of WordPress’s most powerful functions for this: get_the_terms(), wp_list_categories(), and above all, the workhorse known as WP_Query. They each have their own superpower.

The Building Blocks: get_the_terms() and Friends

First, you often need to just get the terms for a specific post. This is your starting point. You’re inside The Loop, looking at a single post, and you want to list its categories or tags. This is where get_the_terms() shines. It’s a clean, object-oriented approach that’s far better than the old, confusing the_tags() or the_category() template tags.

<?php
// Inside your single.php or template part, within The Loop.
$post_id = get_the_ID();
$terms = get_the_terms( $post_id, 'genre' ); // 'genre' is our custom taxonomy

if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
    echo '<ul class="post-genres">';
    foreach ( $terms as $term ) {
        // $term is now a WP_Term object. We can get its name and link.
        $term_link = get_term_link( $term );
        echo '<li><a href="' . esc_url( $term_link ) . '">' . esc_html( $term->name ) . '</a></li>';
    }
    echo '</ul>';
}
?>

Why this is better: You get an array of WP_Term objects. This gives you total control over the HTML output. You’re not stuck with a comma-separated list or a weirdly formatted <div>. You want them in a <ul>? A <span> with custom CSS classes? A JSON-LD script? Go nuts. The old way (the_tags()) was convenient but inflexible. This is the professional’s choice.

Listing All Terms: wp_list_categories() for Taxonomies

Sometimes you don’t want terms for one post; you want a navigational list of all the terms in a taxonomy, like a “Browse by Genre” sidebar. The wp_list_categories() function is built for this, and it’s been upgraded to handle custom taxonomies beautifully.

<h4>Browse by Genre</h4>
<ul>
    <?php
    wp_list_categories( array(
        'taxonomy' => 'genre', // Use your custom taxonomy name
        'title_li' => '', // This crucial parameter REMOVES the default <li>Categories</li> title. I've lost hours of my life to this.
        'show_count' => true, // Show how many posts are in each? Sure, why not.
        'depth' => 1, // Only show top-level terms, no children
    ) );
    ?>
</ul>

The key pitfall here, the one that makes every developer curse at least once, is 'title_li' => ''. If you omit this, the function outputs an <li>Categories</li> item at the top of your list, because the default value is __('Categories'). It’s a bizarre, infuriating default. Set it to an empty string to make it stop.

This is what you came for. The logic is simple: 1) Get the terms of the current post. 2) Use those term IDs to find other posts that share them. 3) Display those posts. The trick is doing it without creating a performance nightmare or showing irrelevant results.

<?php
// Get the terms for the current post
$terms = get_the_terms( get_the_ID(), 'genre' );
if ( empty( $terms ) || is_wp_error( $terms ) ) {
    // If this post has no terms, abort mission. No point running a query.
    return;
}

// Pluck the term IDs from the array of WP_Term objects. WP_Query doesn't want the objects, it wants IDs.
$term_ids = wp_list_pluck( $terms, 'term_id' );

// Now, run the query for related posts
$related_posts_query = new WP_Query( array(
    'post_type' => 'post', // Or your custom post type, e.g., 'movie'
    'tax_query' => array(
        array(
            'taxonomy' => 'genre',
            'field'    => 'term_id',
            'terms'    => $term_ids,
            'operator' => 'IN', // Find posts that are in ANY of these terms
        ),
    ),
    'posts_per_page' => 4, // Get 4 related posts
    'post__not_in'   => array( get_the_ID() ), // CRITICAL: Exclude the current post itself!
    'orderby'        => 'rand', // Mix them up. 'date' is boring for "related" content.
) );

if ( $related_posts_query->have_posts() ) :
    echo '<h3>You Might Also Like</h3>';
    echo '<ul>';
    while ( $related_posts_query->have_posts() ) : $related_posts_query->the_post();
        echo '<li><a href="' . get_permalink() . '">' . get_the_title() . '</a></li>';
    endwhile;
    echo '</ul>';
endif;

// NEVER forget this: reset the post data back to the main query.
// If you don't, your sidebar and other Loop-based elements will break in spectacularly confusing ways.
wp_reset_postdata();
?>

The ‘IN’ vs. ‘AND’ Trap: The operator parameter is vital. IN means “find posts that match any of these terms.” This is usually what you want for related content. AND means “find posts that match all of these terms.” That will drastically narrow the results, often returning nothing. Use AND only if you want posts that are hyper-specific matches.

This code is the blueprint. From here, you can make it as simple or complex as your design requires, but the core logic remains: get terms, query by them, and for the love of all that is holy, don’t forget post__not_in and wp_reset_postdata(). Those two omissions have caused more plugin support forum threads than any other issue in WordPress history. Don’t be that person.