Skip to content

Taxonomies

Intro

Inside Launchpad's /inc/taxonomies.php file there are examples for three kinds of custom taxonomies:

  1. Simple: standard WordPress taxonomy
  2. Select: metabox displays as a select box
  3. PostType: terms are generated by post data, admin page is hidden

All taxonomies should be initialized and managed through the /inc/taxonomies.php file.

Taxonomy Slugs

The slug you use for a custom taxonomy will depend on whether or not the taxonomy is shared between post types, and whether or not it shares a name with a post type.

Single-Use Taxonomies

For taxonomies assigned to only one post type, the slug should be {posttype}-{taxonomy-name}. So in the event of a taxonomy called "Type" used on a post type called "Books," the slug would be books-type.

Hyphenating Taxonomy Slugs

When providing a slug for taxonomies, please use hyphens instead of underscores between words so as to create more commonplace URLs.

Shared Taxonomies

For taxonomies shared across multiple post types, the slug should just be {taxonomy-name}. That means for a taxonomy called "Issues" shared by three post types called "Blogs," "News" and "Reports," the slug would be issues.

Custom Post Type & Taxonomy Slugs

Occasionally, a shared custom taxonomy will have the same name as a custom post type, e.g. "Programs." If the slugs are the same, in this example programs, it will break several pieces of built-in WordPress functionality, including archive filters.

In this situation, please default to a plural slug for the custom post type, and a singular slug for the taxonomy. So that would be programs for the post type and program for the taxonomy.

Simple Taxonomies

These are the most basic of custom taxonomies, and only require a register_taxonomy() function to work.

Simple Tax Registration
<?php 
// Simple: Type [Books] 
$book_tax_args = array(
    'label'        => __( 'Type', 'textdomain' ),
    'public'       => true,
    'rewrite'      => false,
    'hierarchical' => false,
    'show_tagcloud'     => false,
    'show_admin_column' => true,
    // uses wp default hierarchical metabox  
    'meta_box_cb'       => 'post_categories_meta_box'
);
register_taxonomy( 'books-type', array('books'), $book_tax_args );

Hierarchical

Simple taxonomies are marked as non-hierarchical by default — make 'hierarchical' => true, to enable.

Metabox Callback

Non-hierarchical taxnomies display as a tag cloud by default, with the ability to add new terms on the fly. So as to avoid term bloat, we can set 'meta_box_cb' => 'post_categories_meta_box', and simple taxonomies will display using the hierarchical taxonomy metabox.

Select Taxonomies

Select taxonomies initialize much the same as simple taxonomies, but are displayed as <select> elements with the use of custom callback and post save functions. This enforces assigning only one term per post at a time.

Select Tax Registration
<?php 
// Select: Event Type [Events]
$event_type__tax_args = array(
  'label'        => __('Event Type', 'textdomain'),
  'public'            => true,
  'rewrite'           => false,
  'hierarchical'      => false,
  'show_tagcloud'     => false,
  'show_admin_column' => true,
  'meta_box_cb'       => 'event_type__select'
);
register_taxonomy('event-type', array('events'), $event_type__tax_args);

Metabox Callback

In order to create a <select> element for the taxonomy, we must provide a custom callback function.

Select Tax Callback
<?php 
// Select: Event Type [Events] 
function event_type__select($post) {
  $terms = get_terms(array('taxonomy' => 'event-type', 'hide_empty' => false));
  $post  = get_post();
  $post_terms = wp_get_object_terms($post->ID, 'event-type', array('orderby' => 'term_id', 'order' => 'ASC'));
  $active_term  = '';

  if (!is_wp_error($post_terms)) {
    if (isset($post_terms[0]) && isset($post_terms[0]->name)) {
      $active_term = $post_terms[0]->name;
    }
  }
  ?>
  <select name="<?php echo 'event-type'; ?>" id="<?php echo 'event-type'; ?>" class="widefat">
    <?php
    foreach ($terms as $term) {
    ?>
      <option value="<?php esc_attr_e($term->name); ?>" <?php selected($term->name, $active_term); ?>><?php esc_html_e($term->name); ?></option>
    <?php
    }
    ?>
  </select>
  <?php
}

Post Save Action

Since we're creating a custom element for the taxonomy information, we also need to add an action on post save for WordPress to save the assigned terms.

Select Tax Save
<?php 
// Select: Event Type [Events] 
function save__event_type__select($post_id) {
  // ignore autosaves 
  if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
    return;
  }
  if (!isset($_POST['event-type'])) {
    return;
  }
  $tax_items = sanitize_text_field($_POST['event-type']);

  $term = get_term_by('name', $tax_items, 'event-type');
  if (!empty($term) && !is_wp_error($term)) {
    wp_set_object_terms($post_id, $term->term_id, 'event-type', false);
  } else if (empty($term) && !is_wp_error($term)) {
    wp_set_object_terms($post_id, $tax_items, 'event-type', false);
  }
}
add_action('save_post_events', 'save__event_type__select');

Saving on Multiple Post Types

The function above is written to fire when a particular post type is saved (i.e. events), instead of firing on every post type. This is simply safer and more efficient. In the event your taxonomy belongs to multiple post types, you can add another add_action() for each additional post type, replacing the first parameter with the appropriate action slug.

PostType Taxonomies

These taxonomies have their terms generated by the posts of a specified post type, requiring four total functions to register, display, save and sync terms.

The main purpose of these taxonomies is to create direct relationships between posts of different post types. In the example code that follows, we are setting up the taxonomy "Program" to appear on "Stories" posts, but instead of managing the terms like normal, the terms will be automatically created, updated and deleted by managing posts in "Programs" post type.

PostType Tax Registration
<?php 
// PostType: Programs [Stories] 
$story_programs__tax_args = array(
  'label'        => __('Programs', 'textdomain'),
  'public'            => false,
  'publicly_queryable' => true,
  'show_in_nav_menus' => true,
  'show_ui'           => true,
  'show_in_menu'      => false, 
  'rewrite'           => false,
  'hierarchical'      => false,
  'show_tagcloud'     => false,
  'show_admin_column' => true,
  'meta_box_cb'       => 'story_programs__checkbox'
);
register_taxonomy('stories-programs', 'stories', $story_programs__tax_args);

Hiding the Taxonomy UI

In order to make sure that PostType taxonomy terms stay in sync with their related post type, we set 'public' => false, and 'show_in_menu' => false, to hide the associated WP admin pages. However, since 'publicly_queryable', 'show_in_nav_menus' and 'show_ui' inherit their values from the 'public' setting, we must explicitly set them to true in order for the tax to work as expected.

Hidden, but not Gone

If you do need to access a PostType taxonomy's admin page while building, you still can, its link just won't show up in the WordPress sidebar nav.

To access the tax admin page, you can visit https://{site-address}/wp-admin/edit-tags.php?taxonomy={taxonomy-slug}.

Metabox Callback

This callback will create a non-editable list of terms as checkboxes, allowing for multiple terms to be assigned per post. You can also easily adapt the Select Tax Callback to force a single term per post.

PostType Tax Callback
<?php 
// PostType: Programs [Stories] 
function story_programs__checkbox($post) {
  $terms = get_terms('stories-programs', array('hide_empty' => false));
  $post  = get_post();
  $story_programs = wp_get_object_terms($post->ID, 'stories-programs', array('orderby' => 'term_id', 'order' => 'ASC'));

  echo '<div style="margin-top:12px;">';
  foreach ($terms as $term) {
    if (is_array($story_programs) && in_array($term, $story_programs)) {
      $checked = 'checked="checked"';
    } else {
      $checked = null;
    }
  ?>
    <label for="<?php echo 'stories-programs[' . $term->name . ']'; ?>" style="display:inline-block;margin-bottom:8px;">
      <input type="checkbox" name="<?php echo 'stories-programs[]'; ?>" value="<?php echo $term->name; ?>" id="<?php echo 'stories-programs[' . $term->name . ']'; ?>" <?php echo $checked; ?> />
      <?php
      esc_html_e($term->name);
      ?>
    </label>
    <br />
  <?php
  }
  echo '</div>';
}

Post Save Action

As with any taxonomy that utilizes a custom metabox, we must include a custom post save action.

PostType Tax Save
<?php 
// PostType: Programs [Stories] 
function save__story_programs__checkbox($post_id) {
  // ignore autosaves 
  if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
    return;
  }
  if (!isset($_POST['stories-programs'])) {
    $tax_item = array();
  } else {
    $tax_item = $_POST['stories-programs'];
  }

  if (!empty($tax_item)) {
    wp_set_object_terms($post_id, $tax_item, 'stories-programs', false);
  } else {
    wp_set_object_terms($post_id, null, 'stories-programs', false);
  }
}
add_action('save_post_stories', 'save__story_programs__checkbox');

Syncing Posts to Taxonomy Terms

This function is the magic behind the PostType taxonomy. It fires on post save for the associate post type (instead of the post type the taxonomy is assigned to), and compares the title of the post with the current list of taxonomy terms.

In the PostType examples thus far, we've been working with a taxonomy called "Programs," which is assigned to the "Stories" post type, and gets its terms from the "Programs" post type (aka its associated post type). In this case, the following function will fire when a "Programs" post is created, edited or deleted, and will create, edit or delete terms in the "Programs" taxonomy as appropriate.

PostType Tax Sync
<?php 
// PostType: Programs [Stories] 
function add__story_programs__terms($post_ID, $post_after, $post_before) {
  // checking for post type that will create tax terms 
  // end function if post type isn't == 'programs'
  if ($post_after->post_type != 'programs') {
    return;
  }

  // get the current terms in array 
  $terms_compare = array();
  $tax_terms = get_terms('stories-programs', array('hide_empty' => false,));
  foreach ($tax_terms as $term) {
    $terms_compare[] = $term->name;
  }

  // if post was deleted 
  if ($post_after->post_status == 'trash') {
    // check for matching term
    if (in_array($post_after->post_title, $terms_compare)) {
      // delete term
      $post_term = get_term_by('name', $post_after->post_title, 'stories-programs');
      wp_delete_term($post_term->term_id, 'stories-programs');
    }
  }
  // if post was published 
  elseif ($post_after->post_status == 'publish') {
    // if post_title is unchanged 
    if ($post_after->post_title == $post_before->post_title) {
      // check for matching term 
      if (!in_array($post_after->post_title, $terms_compare)) {
        // add term if no match
        wp_insert_term($post_after->post_title, 'stories-programs');
      }
    }
    // if post title was updated
    else {
      // check for matching $post_before->post_title term
      if (in_array($post_before->post_title, $terms_compare)) {
        // update term 
        $post_term = get_term_by('name', $post_before->post_title, 'stories-programs');
        wp_update_term($post_term->term_id, 'stories-programs', array(
          'name' => $post_after->post_title,
          'slug' => sanitize_title($post_after->post_title)
        ));
      } else {
        // add term 
        wp_insert_term($post_after->post_title, 'stories-programs');
      }
    }
  }
}
add_action('post_updated', 'add__story_programs__terms', 10, 3);

Watching Post Title, not Slug

This function is built to watch the post title only, and will update both the corresponding term name and slug when changed. While it can sometimes be helpful to give pages with long titles a shortened URL, we've elected to keep tax term titles and slugs in sync to ensure functionality site-wide, and especially on archives.