Custom taxonomies

Group content with your own hierarchical and flat taxonomies, attach them to post types, and query by term.

  • Intermediate
  • 8 min read
  • Updated 17 Oct 2026

What you will learn

  • Register a taxonomy
  • Choose taxonomy or meta for data
  • Assign and display terms in code
  • Query posts by term

Before you start

A taxonomy is a way to group content. Categories and tags are the two taxonomies WordPress ships with. When events need grouping by type or by speaker, you register your own. Each taxonomy gets an admin screen, archive pages, REST endpoints and efficient querying for free.

Hierarchical and flat taxonomies

Taxonomies come in two shapes:

HierarchicalFlat
LikeCategoriesTags
Terms can have parentsYes, such as Workshops then Beginner workshopsNo
Editor interfaceA checklist of existing termsA free-text field that creates terms as you type
Good forA fixed, curated structureOpen-ended labels

For the events site you will register one of each: a hierarchical myplugin_event_type (Workshops, with Beginner workshops below it, Talks and Meetups) and a flat myplugin_speaker.

Registering a taxonomy

Taxonomies are content structure, so they belong in the plugin, registered on init like post types:

PHP
<?php
// wp-content/plugins/myplugin/includes/taxonomies.php
defined( 'ABSPATH' ) || exit;

add_action( 'init', 'myplugin_register_taxonomies' );

function myplugin_register_taxonomies() {
    register_taxonomy(
        'myplugin_event_type',
        array( 'myplugin_event' ),
        array(
            'labels'            => array(
                'name'          => _x( 'Event Types', 'taxonomy general name', 'myplugin' ),
                'singular_name' => _x( 'Event Type', 'taxonomy singular name', 'myplugin' ),
                'search_items'  => __( 'Search Event Types', 'myplugin' ),
                'all_items'     => __( 'All Event Types', 'myplugin' ),
                'parent_item'   => __( 'Parent Event Type', 'myplugin' ),
                'edit_item'     => __( 'Edit Event Type', 'myplugin' ),
                'add_new_item'  => __( 'Add New Event Type', 'myplugin' ),
                'menu_name'     => __( 'Event Types', 'myplugin' ),
            ),
            'hierarchical'      => true,
            'public'            => true,
            'show_in_rest'      => true,
            'show_admin_column' => true,
            'rewrite'           => array(
                'slug'         => 'event-type',
                'with_front'   => false,
                'hierarchical' => true,
            ),
        )
    );

    register_taxonomy(
        'myplugin_speaker',
        array( 'myplugin_event' ),
        array(
            'labels'            => array(
                'name'                       => _x( 'Speakers', 'taxonomy general name', 'myplugin' ),
                'singular_name'              => _x( 'Speaker', 'taxonomy singular name', 'myplugin' ),
                'search_items'               => __( 'Search Speakers', 'myplugin' ),
                'add_new_item'               => __( 'Add New Speaker', 'myplugin' ),
                'separate_items_with_commas' => __( 'Separate speakers with commas', 'myplugin' ),
                'menu_name'                  => __( 'Speakers', 'myplugin' ),
            ),
            'hierarchical'      => false,
            'public'            => true,
            'show_in_rest'      => true,
            'show_admin_column' => true,
            'rewrite'           => array( 'slug' => 'speaker' ),
        )
    );
}

Load the file from your main plugin file with require_once MYPLUGIN_PATH . 'includes/taxonomies.php';.

The arguments that matter most:

  • The key (myplugin_event_type): prefixed, lower case, at most 32 characters, and never changed once terms exist.
  • Object types (array( 'myplugin_event' )): the post types the taxonomy attaches to. One taxonomy can serve several post types.
  • hierarchical: true for categories-style, false for tags-style.
  • show_in_rest: required for the taxonomy panel to appear in the block editor, and for REST endpoints such as /wp-json/wp/v2/myplugin_event_type.
  • show_admin_column: adds a column for the taxonomy to the events list screen.
  • rewrite: sets term archive URLs. An event type lives at /event-type/workshops/, and with 'hierarchical' => true a child appears at /event-type/workshops/beginner-workshops/.

As with post types, flush rewrite rules once after registering, on plugin activation. Call myplugin_register_taxonomies() in your activation function before flush_rewrite_rules().

Attaching an existing taxonomy

You can also attach a taxonomy to a post type that is registered elsewhere, such as core tags on events:

PHP
// wp-content/plugins/myplugin/includes/taxonomies.php
add_action( 'init', 'myplugin_attach_tags', 20 );

function myplugin_attach_tags() {
    register_taxonomy_for_object_type( 'post_tag', 'myplugin_event' );
}

Priority 20 makes sure the post type exists before you attach to it.

Taxonomy or meta?

The most important modelling decision for custom data is whether it is a term or a meta value. Use this rule of thumb:

QuestionTaxonomyMeta
Will you list or filter content by this value?YesRarely
Is the value shared by many posts?Yes, such as an event typeNo, such as an event date
Does it need its own archive page?YesNo
Is it a free-form or numeric value?NoYes, such as a price
Does it need a description or its own fields?Terms support bothNo

For the events site:

  • Event type: taxonomy. Many events share a type, and visitors browse by it.
  • Speaker: taxonomy. You want an archive of every event one person speaks at.
  • Event date: meta. It is specific to each event and used for sorting and date ranges, not grouping.
  • Price: meta. It is a number, used for filtering and display rather than as a label.

Querying by term is fast, because the term relationship table is indexed for it. Filtering by meta value is slower, as the meta queries lesson explains. If you catch yourself filtering by a meta value on every archive, it probably wants to be a taxonomy.

Working with terms in code

Create terms and assign them with the taxonomy API:

PHP
// wp-content/plugins/myplugin/includes/taxonomies.php
function myplugin_seed_event_types() {
    if ( ! current_user_can( 'manage_categories' ) ) {
        return;
    }

    $workshops = wp_insert_term( 'Workshops', 'myplugin_event_type' );

    if ( ! is_wp_error( $workshops ) ) {
        wp_insert_term(
            'Beginner workshops',
            'myplugin_event_type',
            array( 'parent' => $workshops['term_id'] )
        );
    }

    wp_insert_term( 'Talks', 'myplugin_event_type' );
    wp_insert_term( 'Meetups', 'myplugin_event_type' );
}

wp_insert_term() returns an array with the new term_id, or a WP_Error if, for example, the term already exists. Always check with is_wp_error().

To set the terms on an event:

PHP
// Replace the event's types.
wp_set_object_terms( $event_id, array( 'beginner-workshops' ), 'myplugin_event_type' );

// Add a speaker without removing existing ones (the fourth argument appends).
wp_set_object_terms( $event_id, array( 'Priya Shah' ), 'myplugin_speaker', true );

Strings are matched against existing slugs and names, and a string that matches nothing creates a new term. To be precise, pass integer term IDs instead.

Displaying terms

In a template, get_the_term_list() returns linked term names as HTML. Pass it through wp_kses_post() when you print it, which keeps the links and strips anything unsafe:

PHP
<?php
// wp-content/themes/mytheme/single-myplugin_event.php, inside the Loop
echo wp_kses_post( get_the_term_list( get_the_ID(), 'myplugin_event_type', '<p class="event-types">', ', ', '</p>' ) );

For full control, get_the_terms() returns term objects:

PHP
<?php
$myplugin_speakers = get_the_terms( get_the_ID(), 'myplugin_speaker' );

if ( $myplugin_speakers && ! is_wp_error( $myplugin_speakers ) ) {
    foreach ( $myplugin_speakers as $myplugin_speaker ) {
        printf(
            '<a href="%s">%s</a> ',
            esc_url( get_term_link( $myplugin_speaker ) ),
            esc_html( $myplugin_speaker->name )
        );
    }
}

get_the_terms() returns false when there are no terms and a WP_Error if the taxonomy does not exist, so check both.

To list all terms in a taxonomy, such as a menu of event types, use get_terms():

PHP
<?php
$myplugin_types = get_terms(
    array(
        'taxonomy'   => 'myplugin_event_type',
        'hide_empty' => true,
        'parent'     => 0,
    )
);

'parent' => 0 returns top-level event types only.

Term archives

Each term gets an archive page listing its posts, handled by the main query. The template hierarchy looks for taxonomy-myplugin_event_type-workshops.php, then taxonomy-myplugin_event_type.php, then taxonomy.php, then archive.php and index.php. In a template, get_queried_object() returns the current term, so you can print its name and description.

Querying posts by term

To fetch events of one type yourself, use tax_query in WP_Query:

PHP
<?php
$myplugin_workshops = new WP_Query(
    array(
        'post_type' => 'myplugin_event',
        'tax_query' => array(
            array(
                'taxonomy' => 'myplugin_event_type',
                'field'    => 'slug',
                'terms'    => array( 'workshops' ),
            ),
        ),
    )
);

By default, child terms are included: a query for Workshops also returns Beginner workshops. Add 'include_children' => false to the inner array to prevent that. The meta and tax queries lesson covers combining several conditions.

Summary

  • Register taxonomies in a plugin, on init, with a prefixed key and full labels.
  • Hierarchical taxonomies work like categories; flat ones work like tags.
  • show_in_rest is needed for the block editor panel and REST endpoints.
  • Use a taxonomy for shared values you group and filter by; use meta for unique or numeric values.
  • Manage terms with wp_insert_term() and wp_set_object_terms(); display them with get_the_term_list() and get_the_terms().
  • Query by term with tax_query.

In the next lesson you will write custom queries with WP_Query.

Further reading