Create Custom Taxonomies and Link Them to CPTs in WordPress

September 25, 2025

Custom taxonomies let you group your Custom Post Types (CPTs) with meaningful categories or tags—like Genre for a “Books” CPT, or Skill for a “Portfolio” CPT. Below you’ll learn how to register hierarchical (category-like) and non-hierarchical (tag-like) taxonomies, attach them to CPTs, display them, and query by them.


1) Register a CPT (Example)

We’ll use a simple book CPT to demonstrate. Add to functions.php or a site plugin:

<?php
// 1) Register the "book" CPT
add_action( 'init', function() {
    register_post_type( 'book', array(
        'labels' => array(
            'name' => 'Books',
            'singular_name' => 'Book',
        ),
        'public'       => true,
        'has_archive'  => true,
        'menu_icon'    => 'dashicons-book',
        'supports'     => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
        'show_in_rest' => true, // Gutenberg + REST
        'rewrite'      => array( 'slug' => 'books' ),
    ) );
} );

2) Register Custom Taxonomies

A) Hierarchical taxonomy (category-like): genre

<?php
// 2A) Register "genre" taxonomy and link to "book"
add_action( 'init', function() {
    $labels = array(
        'name'              => 'Genres',
        'singular_name'     => 'Genre',
        'search_items'      => 'Search Genres',
        'all_items'         => 'All Genres',
        'parent_item'       => 'Parent Genre',
        'parent_item_colon' => 'Parent Genre:',
        'edit_item'         => 'Edit Genre',
        'update_item'       => 'Update Genre',
        'add_new_item'      => 'Add New Genre',
        'new_item_name'     => 'New Genre Name',
        'menu_name'         => 'Genres',
    );

    register_taxonomy( 'genre', array( 'book' ), array(
        'labels'            => $labels,
        'hierarchical'      => true,          // category-like
        'show_ui'           => true,
        'show_admin_column' => true,
        'show_in_rest'      => true,          // block editor + REST
        'rewrite'           => array( 'slug' => 'genre' ),
    ) );
} );

B) Non-hierarchical taxonomy (tag-like): writer

<?php
// 2B) Register "writer" taxonomy and link to "book"
add_action( 'init', function() {
    $labels = array(
        'name'                       => 'Writers',
        'singular_name'              => 'Writer',
        'search_items'               => 'Search Writers',
        'popular_items'              => 'Popular Writers',
        'edit_item'                  => 'Edit Writer',
        'update_item'                => 'Update Writer',
        'add_new_item'               => 'Add New Writer',
        'separate_items_with_commas' => 'Separate writers with commas',
        'add_or_remove_items'        => 'Add or remove writers',
        'choose_from_most_used'      => 'Choose from the most used',
        'menu_name'                  => 'Writers',
    );

    register_taxonomy( 'writer', array( 'book' ), array(
        'labels'            => $labels,
        'hierarchical'      => false,         // tag-like
        'show_ui'           => true,
        'show_admin_column' => true,
        'show_in_rest'      => true,
        'rewrite'           => array( 'slug' => 'writer' ),
    ) );
} );

Note: You can attach the same taxonomy to multiple CPTs by listing more post types in the array (e.g., array( 'book','movie' )).


3) Linking a Taxonomy to an Existing CPT

If a taxonomy is registered separately, ensure it’s attached to a post type:

<?php
add_action( 'init', function() {
    register_taxonomy_for_object_type( 'genre', 'book' );
    register_taxonomy_for_object_type( 'writer', 'book' );
} );

4) Flush Permalinks After Adding Taxonomies

  1. Go to Settings → Permalinks.
  2. Click Save Changes (no edits needed).
  3. This refreshes rewrite rules so archive and term URLs work.

5) Display Terms on Single CPT Pages

In your single template (e.g., single-book.php), output assigned terms:

<?php
// Show genres
$terms = get_the_terms( get_the_ID(), 'genre' );
if ( $terms && ! is_wp_error( $terms ) ) {
    echo '<p class="book-genres"><strong>Genres:</strong> ';
    $links = array();
    foreach ( $terms as $t ) {
        $links[] = '<a href="' . esc_url( get_term_link( $t ) ) . '">' . esc_html( $t->name ) . '</a>';
    }
    echo implode( ', ', $links ) . '</p>';
}

// Show writers
$writers = get_the_terms( get_the_ID(), 'writer' );
if ( $writers && ! is_wp_error( $writers ) ) {
    echo '<p class="book-writers"><strong>Writers:</strong> ';
    echo esc_html( join( ', ', wp_list_pluck( $writers, 'name' ) ) );
    echo '</p>';
}
?>

6) Query CPTs by Taxonomy

A) Direct query with WP_Query

<?php
$q = new WP_Query( array(
    'post_type'      => 'book',
    'posts_per_page' => 6,
    'tax_query'      => array(
        array(
            'taxonomy' => 'genre',
            'field'    => 'slug',
            'terms'    => array( 'fantasy', 'sci-fi' ),
        ),
    ),
) );

if ( $q->have_posts() ) :
  echo '<ul class="books">';
  while ( $q->have_posts() ) : $q->the_post();
    echo '<li><a href="' . esc_url( get_permalink() ) . '">' . esc_html( get_the_title() ) . '</a></li>';
  endwhile;
  echo '</ul>';
  wp_reset_postdata();
endif;
?>

B) Modify main query via pre_get_posts (e.g., genre archives show books)

<?php
add_action( 'pre_get_posts', function( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) return;

    // On genre taxonomy archives, show only "book" CPT
    if ( $query->is_tax( 'genre' ) ) {
        $query->set( 'post_type', array( 'book' ) );
    }
} );

7) Template Files for Taxonomy Archives

Create templates to control taxonomy archive layouts:

  • taxonomy-genre.php – for all genre term archives
  • taxonomy-writer.php – for all writer term archives
  • taxonomy-genre-fantasy.php – specific to the fantasy term
<?php // taxonomy-genre.php (example)
get_header(); ?>

<main id="primary">
  <h1><?php single_term_title(); ?></h1>
  <?php if ( have_posts() ) : ?>
    <ul class="book-archive">
    <?php while ( have_posts() ) : the_post(); ?>
      <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
    <?php endwhile; ?>
    </ul>
    <?php the_posts_pagination(); ?>
  <?php else : ?>
    <p>No books found in this genre.</p>
  <?php endif; ?>
</main>

<?php get_footer(); ?>

8) Add Admin Columns for Taxonomies (Nice UX)

<?php
// Show Genre column in "Books" list table
add_filter( 'manage_book_posts_columns', function( $cols ) {
    $cols['genre'] = 'Genres';
    return $cols;
} );

add_action( 'manage_book_posts_custom_column', function( $col, $post_id ) {
    if ( $col === 'genre' ) {
        $terms = get_the_terms( $post_id, 'genre' );
        if ( $terms && ! is_wp_error( $terms ) ) {
            echo esc_html( join( ', ', wp_list_pluck( $terms, 'name' ) ) );
        } else {
            echo '—';
        }
    }
}, 10, 2 );

9) Security & REST Tips

  • show_in_rest: Set to true for block editor support and REST endpoints (e.g., /wp-json/wp/v2/genre).
  • Capabilities: For advanced permissions, define capabilities in register_taxonomy() and manage roles accordingly.
  • Validation: When saving terms programmatically, sanitize inputs and verify nonces.

10) Summary

  1. Register your CPT (e.g., book).
  2. Create taxonomies with register_taxonomy() (hierarchical or tag-like).
  3. Attach taxonomies to CPTs (in the register_taxonomy() call or with register_taxonomy_for_object_type()).
  4. Flush permalinks once.
  5. Display terms on single templates; query CPTs with tax_query.
  6. Customize taxonomy archive templates and improve admin UX with columns.

With custom taxonomies linked to your CPTs, you’ll have clean, structured content that’s easier to manage, navigate, and scale.

Avatar

Written by

satoshi

I’ve been building and customizing WordPress themes for over 10 years. In my free time, you’ll probably find me enjoying a good football match.