WordPress Custom Taxonomies – Mastering Advanced WordPress Development Series

WordPress Hooks and Filters – Mastering Advanced WordPress Development Series

Categories and tags get you far. Then one day they don’t. You’re running a recipe site and you want to sort by cuisine, or a directory and you need to file listings by region. Bending tags to do that job always feels like a hack. That’s the moment custom taxonomies earn their keep.

This entry in the Mastering Advanced WordPress Development series shows you how to build one. We’ll cover what a taxonomy actually is, when a custom one pays off, and the exact code to register it, including the couple of gotchas that trip people up the first time.

Part 1: What are WordPress custom taxonomies?

A taxonomy is just a system for grouping content. WordPress ships with two: categories and tags. Categories are hierarchical, so they nest into parent and child, which suits broad topic buckets. Tags are flat, a loose pile of keywords with no structure.

A custom taxonomy is one you define yourself. You pick the name, the terms, and whether it behaves like a category or a tag. The point is a grouping that fits your content instead of forcing your content into someone else’s shape.

Part 2: Creating a WordPress custom taxonomy

Step 1: Register the taxonomy

You register a taxonomy with register_taxonomy(), and it has to run on the init hook. Do it any earlier and WordPress isn’t ready; do it any later and the rewrite rules and admin UI won’t wire up. Register it before or alongside any custom post type you plan to attach it to, and name that post type in the second argument.

Drop this in your theme’s functions.php or, better for anything you want to keep, a small site plugin so it survives a theme switch.

PHP
<?php
/**
 * Registers a custom taxonomy for posts
 */
function custom_taxonomy() {
// Set the labels for the custom taxonomy.
$labels = array(
    &#039;name&#039;                       =&gt; esc_html__( &#039;Name&#039;, &#039;taxonomy general name&#039; ), // The plural name for the taxonomy. Default is &#039;Categories&#039;/&#039;Tags&#039;.
    &#039;singular_name&#039;              =&gt; esc_html__( &#039;Singular Name&#039;, &#039;taxonomy singular name&#039; ), // The singular name for the taxonomy. Default is &#039;Category&#039;/&#039;Tag&#039;.
    &#039;search_items&#039;               =&gt; esc_html__( &#039;Search Items&#039; ), // The label for the search items field. Default is &#039;Search Categories&#039;/&#039;Search Tags&#039;.
    &#039;popular_items&#039;              =&gt; esc_html__( &#039;Popular Items&#039; ), // The label for the popular items field. Default is &#039;Popular Categories&#039;/&#039;Popular Tags&#039;.
    &#039;all_items&#039;                  =&gt; esc_html__( &#039;All Items&#039; ), // The label for the all items field. Default is &#039;All Categories&#039;/&#039;All Tags&#039;.
    &#039;parent_item&#039;                =&gt; esc_html__( &#039;Parent Item&#039; ), // The label for the parent item field. Default is &#039;Parent Category&#039;/&#039;Parent Tag&#039;.
    &#039;parent_item_colon&#039;          =&gt; esc_html__( &#039;Parent Item:&#039; ), // The label for the parent item field with colon. Default is &#039;Parent Category:&#039;/&#039;Parent Tag:&#039;.
    &#039;edit_item&#039;                  =&gt; esc_html__( &#039;Edit Item&#039; ), // The label for the edit item field. Default is &#039;Edit Category&#039;/&#039;Edit Tag&#039;.
    &#039;view_item&#039;                  =&gt; esc_html__( &#039;View Item&#039; ), // The label for the view item field. Default is &#039;View Category&#039;/&#039;View Tag&#039;.
    &#039;update_item&#039;                =&gt; esc_html__( &#039;Update Item&#039; ), // The label for the update item field. Default is &#039;Update Category&#039;/&#039;Update Tag&#039;.
    &#039;add_new_item&#039;               =&gt; esc_html__( &#039;Add New Item&#039; ), // The label for the add new item field. Default is &#039;Add New Category&#039;/&#039;Add New Tag&#039;.
    &#039;new_item_name&#039;              =&gt; esc_html__( &#039;New Item Name&#039; ), // The label for the new item name field. Default is &#039;New Category Name&#039;/&#039;New Tag Name&#039;.
    &#039;separate_items_with_commas&#039; =&gt; esc_html__( &#039;Separate Items with Commas&#039; ), // The label for the separate items with commas field. Default is &#039;Separate Categories with commas&#039;/&#039;Separate Tags with commas&#039;.
    &#039;add_or_remove_items&#039;        =&gt; esc_html__( &#039;Add or Remove Items&#039; ), // The label for the add or remove items field. Default is &#039;Add or remove categories&#039;/&#039;Add or remove tags&#039;.
    &#039;choose_from_most_used&#039;      =&gt; esc_html__( &#039;Choose from the Most Used&#039; ), // The label for the choose from the most used field. Default is &#039;Choose from the most used categories&#039;/&#039;Choose from the most used tags&#039;.
    &#039;not_found&#039;                  =&gt; esc_html__( &#039;Not Found&#039; ), // The label for the not found field. Default is &#039;No categories found&#039;/&#039;No tags found&#039;.
    &#039;no_terms&#039;                   =&gt; esc_html__( &#039;No Items&#039; ), // The label for the no terms field. Default is &#039;No categories&#039;/&#039;No tags&#039;.
    &#039;menu_name&#039;                  =&gt; esc_html__( &#039;Menu Name&#039; ), // The label for the menu name field. Default is the same as &#039;name&#039;.
    &#039;name_admin_bar&#039;             =&gt; esc_html__( &#039;Name Admin Bar&#039; ), // The label for the name displayed in the admin bar. Default is the same as &#039;singular_name&#039;.
);

// Set the arguments for the custom taxonomy.
$args = array(
    &#039;labels&#039;                =&gt; $labels, // (array) An array of labels for the taxonomy. Default is an empty array.
    &#039;public&#039;                =&gt; true, // (bool) Whether the taxonomy should be publicly queryable. Default is true.
    &#039;show_ui&#039;               =&gt; true, // (bool) Whether to show the taxonomy user interface. Default is true.
    &#039;show_in_nav_menus&#039;     =&gt; true, // (bool) Whether the taxonomy should be available in navigation menus. Default is true.
    &#039;show_tagcloud&#039;         =&gt; true, // (bool) Whether the taxonomy should be displayed in the tag cloud widget. Default is true.
    &#039;meta_box_cb&#039;           =&gt; null, // (callable) A callback function to render the meta box for this taxonomy. Default is null.
    &#039;show_admin_column&#039;     =&gt; false, // (bool) Whether to show the taxonomy in the admin column. Default is false.
    &#039;description&#039;           =&gt; &#039;&#039;, // (string) A short description of the taxonomy. Default is an empty string.
    &#039;hierarchical&#039;          =&gt; false, // (bool) Whether the taxonomy should be hierarchical (like categories) or flat (like tags). Default is false.
    &#039;update_count_callback&#039; =&gt; &#039;&#039;, // (callable) A callback function to update the term count for this taxonomy. Default is an empty string.
    &#039;query_var&#039;             =&gt; true, // (bool|string) Whether to allow querying of this taxonomy via the URL. Default is true.
    &#039;rewrite&#039;               =&gt; true, // (bool|array) Whether to allow URL rewriting for this taxonomy, and what the URL should look like. Default is true.
    &#039;sort&#039;                  =&gt; false, // (bool) Whether to sort the terms in this taxonomy. Default is false.
    &#039;capabilities&#039;          =&gt; array(), // (array) An array of capabilities for this taxonomy. Default is an empty array.
    &#039;show_in_rest&#039;          =&gt; false, // (bool) Whether the taxonomy should be available in the REST API. Default is false.
    &#039;rest_base&#039;             =&gt; null, // (string) The base URL for the REST API route. Default is null, which means the taxonomy name is used.
    &#039;rest_controller_class&#039; =&gt; &#039;WP_REST_Terms_Controller&#039;, // (string) The class to use for the REST API controller. Default is &#039;WP_REST_Terms_Controller&#039;.
);

// Register the custom taxonomy.
register_taxonomy( &#039;custom_taxonomy&#039;, array( &#039;post&#039; ), $args );
} // Add the custom taxonomy to the WordPress init hook add_action( 'init', 'custom_taxonomy' );

Make it yours by changing two things. Swap 'custom_taxonomy' in the register_taxonomy() call for your own slug, and edit the label strings so the admin screens read the way you want. The second argument, array( 'post' ), is the list of post types the taxonomy attaches to, so add your own post type there if you’re not using plain posts.

Two arguments in that $args array are worth a second look:

hierarchical decides the whole feel of the thing. Set it to true and you get category-style nesting with parent and child terms and checkboxes in the editor. Leave it false and it behaves like tags, a comma-separated free-for-all. Pick based on your content, not habit.

show_in_rest is set to false in the example above, and that has a real consequence: your taxonomy won’t show up in the block editor. If you’re on Gutenberg and want the meta box in the sidebar, set show_in_rest to true. Miss this and you’ll swear the code is broken when it’s just hidden.

One more thing the snippet leaves out. Custom rewrite rules for your taxonomy’s archive URLs don’t take effect until the rewrite rules are flushed. During development a visit to Settings > Permalinks does it by hand. For a plugin, call flush_rewrite_rules() on activation, after your taxonomy is registered, and never on init on every page load, which is a known performance mistake.

Step 2: Use your custom taxonomy

Once it’s registered, the taxonomy shows up when you edit a post of the type you attached it to. For a hierarchical taxonomy you’ll see a checkbox box like Categories; for a flat one, a text field like Tags. Add your terms there, save, and your content is grouped.

Wrapping up

That’s the whole job: register the taxonomy on init, decide hierarchical or flat, and mind the two quiet defaults, show_in_rest for the block editor and flushing rewrite rules for clean URLs. Get those right and you’ve got a categorization system that actually matches the site you’re building instead of the one WordPress guessed you wanted.

Leave a Comment

Your email address will not be published. Required fields are marked *


Scroll to Top