What taxonomies are for
A taxonomy is a way to group content. The built-in ones are categories and tags. Custom post types often need their own: recipes grouped by course (starter, main, dessert) and tagged by ingredient. A taxonomy belongs to one or more post types, and it is made of terms.
This lesson continues the Bakery Recipes plugin from Custom post types.
Registering taxonomies
Add two taxonomies in the same plugin, on init:
<?php
add_action( 'init', function () {
// Like categories: hierarchical, can have parents.
register_taxonomy( 'course', [ 'recipe' ], [
'labels' => [
'name' => 'Courses',
'singular_name' => 'Course',
],
'hierarchical' => true,
'public' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'course' ],
] );
// Like tags: flat, typed in freely.
register_taxonomy( 'ingredient', [ 'recipe' ], [
'labels' => [
'name' => 'Ingredients',
'singular_name' => 'Ingredient',
],
'hierarchical' => false,
'public' => true,
'show_in_rest' => true,
'rewrite' => [ 'slug' => 'ingredient' ],
] );
} );Load the file from the main plugin file, and rebuild the rewrite rules once so the new addresses work:
<?php
require_once WP_PLUGIN_DIR . '/bakery-recipes/taxonomies.php';wp rewrite flush
wp taxonomy list --object_type=recipe --fields=name,label,hierarchicalSuccess: Rewrite rules flushed.
+------------+-------------+--------------+
| name | label | hierarchical |
+------------+-------------+--------------+
| course | Courses | 1 |
| ingredient | Ingredients | |
+------------+-------------+--------------+The arguments are similar to a post type: labels, public, show_in_rest (needed for the block editor and the REST API), rewrite for the address, and hierarchical. The second argument of register_taxonomy() lists the post types the taxonomy attaches to.
Creating and assigning terms
wp term create course Starters --slug=starters
wp term create course Mains --slug=mains
wp term create course Soups --slug=soups --parent=3Success: Created course 2.
Success: Created course 3.
Success: Created course 4.The recipe posts do not exist yet in this fresh site, so create them and assign terms in code:
<?php
$sourdough = wp_insert_post( [ 'post_type' => 'recipe', 'post_title' => 'Sourdough loaf', 'post_status' => 'publish' ] );
$soup = wp_insert_post( [ 'post_type' => 'recipe', 'post_title' => 'Leek soup', 'post_status' => 'publish' ] );
wp_set_object_terms( $sourdough, [ 'starters' ], 'course' );
wp_set_object_terms( $sourdough, [ 'flour', 'water', 'salt' ], 'ingredient' ); // created if missing
wp_set_object_terms( $soup, [ 'soups' ], 'course' );
wp_set_object_terms( $soup, [ 'leek', 'salt' ], 'ingredient' );
foreach ( [ $sourdough, $soup ] as $id ) {
$courses = wp_list_pluck( get_the_terms( $id, 'course' ), 'name' );
$ingredients = wp_list_pluck( get_the_terms( $id, 'ingredient' ), 'name' );
echo get_the_title( $id ), ' | ', implode( ', ', $courses ), ' | ', implode( ', ', $ingredients ), "\n";
}Sourdough loaf | Starters | flour, salt, water
Leek soup | Soups | leek, saltwp_set_object_terms( $post_id, $terms, $taxonomy ) replaces the post's terms in that taxonomy (pass true as the fourth argument to append instead). For hierarchical taxonomies, pass term IDs to be exact; names and slugs are matched or created. get_the_terms() returns term objects, or false if none, so check before looping in real code.
Term lists and archives
<?php
$terms = get_terms( [ 'taxonomy' => 'ingredient', 'hide_empty' => true, 'orderby' => 'count', 'order' => 'DESC', 'number' => 3 ] );
foreach ( $terms as $term ) {
echo $term->name, ' (', $term->count, ') ', get_term_link( $term ), "\n";
}salt (2) http://localhost:8080/ingredient/salt/
flour (1) http://localhost:8080/ingredient/flour/
water (1) http://localhost:8080/ingredient/water/Every term gets its own archive page, at the address you set in rewrite:
GET /ingredient/salt/Sourdough loaf
Leek soupIn a classic theme you control the layout with taxonomy-ingredient.php or taxonomy.php (see the template hierarchy); in a block theme, taxonomy-ingredient.html.
Filtering by terms
Use tax_query in WP_Query to find posts by their terms:
<?php
$query = new WP_Query( [
'post_type' => 'recipe',
'fields' => 'ids',
'no_found_rows' => true,
'tax_query' => [
'relation' => 'AND',
[ 'taxonomy' => 'ingredient', 'field' => 'slug', 'terms' => 'salt' ],
[ 'taxonomy' => 'course', 'field' => 'slug', 'terms' => 'mains' , 'include_children' => true ],
],
] );
echo "mains with salt: ", count( $query->posts ), "\n";
$query = new WP_Query( [
'post_type' => 'recipe', 'fields' => 'ids', 'no_found_rows' => true,
'tax_query' => [ [ 'taxonomy' => 'course', 'field' => 'slug', 'terms' => 'mains', 'include_children' => true ] ],
] );
echo "in mains (and its children): ", implode( ', ', array_map( 'get_the_title', $query->posts ) ), "\n";mains with salt: 1
in mains (and its children): Leek soupLeek soup is in the child term "Soups", and include_children (on by default) makes a search for its parent "Mains" find it too.
Seeing taxonomies in the dashboard
With show_in_rest enabled, each taxonomy appears as a panel in the block editor's sidebar when you edit a recipe: a checklist for the hierarchical one, and a free-text box with suggestions for the flat one. A menu entry under Recipes opens a screen where editors add, rename and delete terms, and the Terms screen shows how many posts use each. Because terms are shared by all posts of the type, renaming one in a single place updates every recipe at once.
Taxonomy or post meta?
Use a taxonomy for values shared by many posts that visitors browse by: course, ingredient, difficulty. Use post meta for values that belong to one post: cooking time, a price. A taxonomy gives you archive pages, term lists and fast filtering for free.
Common mistakes
- Forgetting
show_in_rest, so the block editor shows no panel. - Registering the taxonomy without the post type (second argument), so it appears nowhere.
- Not flushing rewrite rules, which gives 404 on term pages.
- Using a taxonomy for one-off values that really are meta.
- Using names that clash with built-in query variables (
year,name,page...).
Exercise
Register a non-hierarchical difficulty taxonomy for recipes with the terms Easy, Medium and Hard, and assign "Easy" to the sourdough loaf.
Show one possible answer
add_action( 'init', function () {
register_taxonomy( 'difficulty', [ 'recipe' ], [
'labels' => [ 'name' => 'Difficulty', 'singular_name' => 'Difficulty' ],
'public' => true,
'show_in_rest' => true,
] );
} );
// Once, after the taxonomy exists:
foreach ( [ 'Easy', 'Medium', 'Hard' ] as $name ) {
wp_insert_term( $name, 'difficulty' );
}
wp_set_object_terms( $sourdough_id, 'easy', 'difficulty' );Key points
- A taxonomy groups content: categories and tags are the built-in ones; register_taxonomy() adds your own.
- Hierarchical taxonomies behave like categories (parents and children); non-hierarchical ones behave like tags.
- Register taxonomies on init with the post types they attach to, and set show_in_rest so the block editor shows them.
- Use wp_set_object_terms(), get_the_terms() and the tax_query argument of WP_Query to assign, read and filter by terms.