register_taxonomy()
Register a taxonomy, attach it to post types, and control its REST exposure, admin UI and permalink slug.
register_taxonomy( string $taxonomy, array|string $object_type, array|string $args = array() ): WP_Taxonomy|WP_Errorregister_taxonomy() creates a taxonomy or modifies an existing one. It must run on or after the init hook — earlier and the registry is not ready; later and the rewrite rules and metaboxes have already been built. The key is capped at 32 characters and may contain only lowercase letters, digits, dashes and underscores. Since WordPress 5.4 it returns the WP_Taxonomy object, or a WP_Error.
Attaching to object types
The second parameter is a post type name or an array of them. Calling register_taxonomy() again for an existing taxonomy overwrites the original $object_type, which is why plugins that only want to add one more post type use register_taxonomy_for_object_type() instead.
Arguments that carry weight
public is a shorthand: publicly_queryable, show_ui and show_in_nav_menus all inherit from it when unset, and show_in_menu, show_tagcloud and show_in_quick_edit inherit from show_ui. show_in_rest defaults to false and is what puts the taxonomy panel in the block editor and the route under /wp/v2/; rest_base (4.7) renames that route and rest_namespace (5.9) moves it. rewrite accepts true or an array of slug, with_front, hierarchical and ep_mask. default_term (5.5) creates a fallback term applied when an object is saved with none.
Example
<?php
add_action( 'init', 'acme_register_genre_taxonomy' );
function acme_register_genre_taxonomy() {
register_taxonomy(
'acme_genre', // Key: max 32 chars, [a-z0-9_-] only.
array( 'book', 'post' ), // Object types it is attached to.
array(
'labels' => array(
'name' => __( 'Genres', 'acme' ),
'singular_name' => __( 'Genre', 'acme' ),
),
'description' => __( 'Editorial genre for books.', 'acme' ),
'hierarchical' => true, // Checkbox tree; terms may have a parent.
'public' => true, // publicly_queryable / show_ui /
// show_in_nav_menus inherit from this.
'show_admin_column' => true, // Adds a Genres column to the list table.
'show_in_rest' => true, // Needed for the block editor panel
// and the REST route. Default false.
'rest_base' => 'genres', // /wp/v2/genres, not /wp/v2/acme_genre
'rewrite' => array(
'slug' => 'genre', // /genre/crime/ not /acme_genre/crime/
'with_front' => false, // Ignore the blog permalink prefix.
'hierarchical' => true, // /genre/fiction/crime/ for children.
),
'capabilities' => array(
'manage_terms' => 'manage_categories',
'edit_terms' => 'manage_categories',
'delete_terms' => 'manage_categories',
'assign_terms' => 'edit_posts', // The one authors need.
),
'default_term' => array( // Since WP 5.5.
'name' => __( 'Unclassified', 'acme' ),
'slug' => 'unclassified',
),
)
);
}When to use it
- Adding a Genre or Region vocabulary to a custom post type
- Exposing a taxonomy to the block editor and the REST API
- Changing a taxonomy's public URL prefix without renaming its key
- Giving a taxonomy its own capability set so authors can assign but not create terms
More examples
'rewrite' => true and the array it expands to
Partial arrays are merged over these defaults, so supplying only 'slug' leaves with_front true and hierarchical false. That last default is the usual surprise: a hierarchical taxonomy still produces flat term URLs unless you set rewrite.hierarchical yourself.
<?php
// Shorthand: true is the default value of the argument.
register_taxonomy( 'acme_topic', 'post', array(
'public' => true,
'rewrite' => true,
) );
// Longhand: exactly the values WP_Taxonomy fills in for you when
// 'rewrite' is true. Identical registration.
register_taxonomy( 'acme_topic', 'post', array(
'public' => true,
'rewrite' => array(
'slug' => 'acme_topic', // sanitised taxonomy key
'with_front' => true,
'hierarchical' => false, // note: not inherited from the taxonomy
'ep_mask' => EP_NONE,
),
) );
// 'rewrite' => false turns off pretty term archives entirely; the terms
// remain reachable at ?acme_topic=slug while 'query_var' is set.Adding a post type without clobbering the registration
Three ways to associate a taxonomy with a post type, only two of which are safe from another plugin. Use register_taxonomy_for_object_type() whenever you do not own the original registration; use the 'taxonomies' argument when you own the post type.
<?php
// WRONG in a second plugin: this overwrites $object_type, so 'book'
// silently loses the taxonomy.
// register_taxonomy( 'acme_genre', 'review' );
// Right: additive, and safe to call on init at priority 11 or later,
// after the owning plugin has registered the taxonomy.
add_action( 'init', function () {
register_taxonomy_for_object_type( 'acme_genre', 'review' );
}, 11 );
// register_post_type() can also declare the link from the other side:
register_post_type( 'review', array(
'public' => true,
'taxonomies' => array( 'acme_genre', 'post_tag' ),
) );Private taxonomy for internal workflow state
public => false alone would hide the admin UI too, since show_ui inherits from it; setting show_ui back to true gives you an editorial-only taxonomy. Reach for this for statuses, review flags or routing keys that must never appear on the front end.
<?php
add_action( 'init', function () {
register_taxonomy( 'acme_workflow', 'book', array(
'label' => __( 'Workflow', 'acme' ),
'public' => false, // no archives, no nav menu entry
'show_ui' => true, // but still editable in wp-admin
'show_in_menu' => true,
'show_admin_column' => true,
'show_in_rest' => false, // keep it out of /wp/v2
'hierarchical' => false,
'rewrite' => false,
'query_var' => false, // not loadable via ?acme_workflow=...
'capabilities' => array(
'manage_terms' => 'edit_others_posts',
'edit_terms' => 'edit_others_posts',
'delete_terms' => 'edit_others_posts',
'assign_terms' => 'edit_posts',
),
) );
} );
Discussion