Site icon WP Smith

Custom Rewrite Rules for Custom Post Types and Taxonomies

Getting custom URL structures that differ from standard WordPress convention can be tough, even confusing.

When you register a custom post type, you get these URLs:

When you register a custom taxonomy, you get these URLs:

So, if you want anything else, you need to do something custom. For example, if you want date archive pages with your custom post type or if you want to introduce a taxonomy term in the URL, you must add some custom code.

Setup

So let's setup a plugin main file. First create a folder called post-type-taxonomy-rewrite.

<?php
/**
* Plugin Name: WPS Post Type Taxonomy Rewrite
* Plugin URI: https://wpsmith.net
* Description: Rewrite for {taxonomy}/{postname} rewrites.
* Author: Travis Smith <t@wpsmith.net>
* Author URI: https://wpsmith.net
* Text Domain: wps-rewrite
* Domain Path: /languages
* Version: 0.1.0
*/
/**
* Plugin main file.
*
* @package WPS\Plugins\Rewrite
* @author Travis Smith <t@wpsmith.net>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;

Create a composer.json file where we can require my rewrite package (wpsmith/rewrite) via composer.

{
"name": "wpsmith/post-type-taxonomy-rewrite",
"description": "Rewrite for {taxonomy}/{postname} rewrites.",
"type": "project",
"license": "GPLv2+",
"authors": [
{
"name": "Travis Smith",
"email": "t@wpsmith.net"
}
],
"minimum-stability": "dev",
"require": {
"wpsmith/rewrite": "dev-master"
}
}
view raw composer.json hosted with ❤ by GitHub

Once we have this file, we can do a composer install which will install our packages into a folder called vendor.

Now in the plugin file (post-type-taxonomy-rewrite.php), we need to require the composer autoloader.

// Add autoloader.
require 'vendor/autoload.php';

So now we should have this:

post-type-taxonomy-rewrite/
|- composer.json
|- post-type-taxonomy-rewrite.php
|- vendor/

Custom Post Type / Taxonomy Example

For example, let's say we want this pattern(domain.com/{post-type-slug}/{term}/{postname}):

Now, I have written a class that you can use to make this extremely easy!

Setup

Now, create a resources.php file to contain our post type and taxonomy registration code for resource and resource-type.

<?php
/**
* Post Type and Taxonomy Registration.
*
* Resource Post Type & Resource Type Taxonomy.
*
* @package WPS\Plugins\Rewrite
* @author Travis Smith <t@wpsmith.net>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_tax_resource_type' );
/**
* Register the Resource Type taxonomy
*
*/
function register_tax_resource_type() {
$labels = array(
'name' => __( 'Resource Types', 'wps' ),
'singular_name' => __( 'Type', 'wps' ),
'search_items' => __( 'Search Types', 'wps' ),
'popular_items' => __( 'Popular Types', 'wps' ),
'all_items' => __( 'All Types', 'wps' ),
'parent_item' => __( 'Parent Type', 'wps' ),
'parent_item_colon' => __( 'Parent Type:', 'wps' ),
'edit_item' => __( 'Edit Type', 'wps' ),
'update_item' => __( 'Update Type', 'wps' ),
'add_new_item' => __( 'Add New Type', 'wps' ),
'new_item_name' => __( 'New Type', 'wps' ),
'separate_items_with_commas' => __( 'Separate Types with commas', 'wps' ),
'add_or_remove_items' => __( 'Add or remove Types', 'wps' ),
'choose_from_most_used' => __( 'Choose from most used Types', 'wps' ),
'menu_name' => __( 'Types', 'wps' ),
);
$args = array(
'labels' => $labels,
'public' => true,
'show_in_nav_menus' => true,
'show_ui' => true,
'show_tagcloud' => false,
'hierarchical' => true,
'rewrite' => array( 'slug' => 'resource-type', 'with_front' => false ),
'query_var' => true,
'show_admin_column' => true,
);
register_taxonomy( 'resource_type', array( 'resource' ), $args );
}
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_cpt_resource' );
/**
* Register the custom post type
*
* @since 1.2.0
*/
function register_cpt_resource() {
$labels = array(
'name' => __( 'Resources', 'wps' ),
'singular_name' => __( 'Resource', 'wps' ),
'add_new' => __( 'Add New', 'wps' ),
'add_new_item' => __( 'Add New Resource', 'wps' ),
'edit_item' => __( 'Edit Resource', 'wps' ),
'new_item' => __( 'New Resource', 'wps' ),
'view_item' => __( 'View Resource', 'wps' ),
'search_items' => __( 'Search Resources', 'wps' ),
'not_found' => __( 'No Resources found', 'wps' ),
'not_found_in_trash' => __( 'No Resources found in Trash', 'wps' ),
'parent_item_colon' => __( 'Parent Resource:', 'wps' ),
'menu_name' => __( 'Resources', 'wps' ),
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'supports' => array( 'title', 'editor', 'thumbnail', 'revisions', 'author', 'comments', 'discussion', 'page-attributes' ),
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
'has_archive' => true,
'query_var' => true,
'can_export' => true,
'rewrite' => array( 'slug' => 'resource', 'with_front' => false ),
'menu_icon' => 'dashicons-format-aside',
);
register_post_type( 'resource', $args );
}
view raw resources.php hosted with ❤ by GitHub

So now we should have this:

post-type-taxonomy-rewrite/
|- composer.json
|- post-type-taxonomy-rewrite.php
|- resources.php
|- vendor/

Doing the Rewrites

Then let's create a function:

/**
* Does the resources rewrites.
*/
function do_resources() {
// Require post types file.
require_once 'resources.php';
// Do the rewrite for resources.
try {
// Create the rewrite object connecting the post type and taxonomy.
$resource_resource_type = new \WPS\Rewrite\PostTypeByTaxonomy( array(
'post_type' => 'resource',
'taxonomy' => 'resource_type',
) );
// Set the order of the rewrite to `%post_type%/%term%`. Defaults to `%term%/%post_type%`.
$resource_resource_type->set_order( [
'%post_type%',
'%term%',
] );
// Add all the rewrites. This includes the main, embed, feed, pagination, and date URLs.
$resource_resource_type->add_all_rewrites();
} catch ( \Exception $e ) {
// do nothing right now.
// @todo Maybe do something.
}
}
// Do it!
do_resources();

That's it!

On Activation

Because this is a plugin, we need to flush the rewrite rules when the plugin is activated. So in the plugin file (post-type-taxonomy-rewrite.php), let's flush the rules.

register_activation_hook( __FILE__, '\WPS\Plugins\Rewrite\PostTypeTaxonomy\on_activation' );
/**
* Flush rules on activation.
*/
function on_activation() {
// Registering Resources.
register_tax_resource_type();
register_cpt_resource();
// Flush the rules.
flush_rewrite_rules();
}

Taxonomy / Custom Post Type Example

What if you have a hierarchical post type that you wanted to use with this pattern (domain.com/{term}/{post-type-slug}/{postname}):

Setup

Now, create a landing-pages.php file to contain our post type and taxonomy registration code for landing_page and campaign_type (this replaces resources.php).

<?php
/**
* Post Type and Taxonomy Registration.
*
* Landing Page Post Type & Campaign Type Taxonomy.
*
* @package WPS\Plugins\Rewrite
* @author Travis Smith <t@wpsmith.net>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_cpt_landing_pages' );
/**
* Register the Landing Pages Post Type.
*/
function register_cpt_landing_pages() {
$labels = array(
'name' => __( 'Landing Pages', 'wps' ),
'singular_name' => __( 'Landing Page', 'wps' ),
);
$args = array(
'label' => __( 'Landing Pages', 'wps' ),
'labels' => $labels,
'description' => '',
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'delete_with_user' => false,
'show_in_rest' => true,
'rest_base' => '',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'has_archive' => false,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'exclude_from_search' => true,
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => array( 'slug' => 'landing-page', 'with_front' => true ),
'query_var' => true,
'menu_position' => 5,
'supports' => array( 'title', 'editor', 'thumbnail' ),
'taxonomies' => array( 'campaign_type' ),
'menu_icon' => 'dashicons-analytics',
);
register_post_type( 'landing_page', $args );
}
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_tax_campaign_type' );
/**
* Register the Campaign Type Custom Taxonomy.
*/
function register_tax_campaign_type() {
$labels = array(
'name' => __( 'Campaign Types', 'wps' ),
'singular_name' => __( 'Campaign Type', 'wps' ),
);
$args = array(
'label' => __( 'Campaign Types', 'wps' ),
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'hierarchical' => true,
'show_ui' => true,
'show_in_menu' => true,
'show_in_nav_menus' => true,
'query_var' => true,
'rewrite' => array( 'slug' => 'campaign_type', 'with_front' => true, ),
'show_admin_column' => false,
'show_in_rest' => true,
'rest_base' => 'campaign_type',
'rest_controller_class' => 'WP_REST_Terms_Controller',
'show_in_quick_edit' => false,
);
register_taxonomy( 'campaign_type', array( 'landing_page' ), $args );
// Make taxonomy single term only.
// 'type' can be 'radio' or 'select' (default: radio)
new \WPS\Taxonomies\SingleTermTaxonomy( 'campaign_type', array( 'landing_page' ), 'radio' );
}

So now we should have this:

post-type-taxonomy-rewrite/
|- composer.json
|- post-type-taxonomy-rewrite.php
|- landing-pages.php
|- vendor/

With Landing Pages, I added an additional class SingleTermTaxonomy to ensure that the taxonomy, campaign_type would always have only one term selected. In order to use this additional class, composer.json needs to be updated.

{
"name": "wpsmith/post-type-taxonomy-rewrite",
"description": "Rewrite for {taxonomy}/{postname} rewrites.",
"type": "project",
"license": "GPLv2+",
"authors": [
{
"name": "Travis Smith",
"email": "t@wpsmith.net"
}
],
"minimum-stability": "dev",
"require": {
"wpsmith/single-term-taxonomy": "dev-master",
"wpsmith/rewrite": "dev-master"
}
}

SingleTermTaxonomy is not required, and if you do not wish to use this class, then simply delete lines 88-89.

new \WPS\Taxonomies\SingleTermTaxonomy( 'campaign_type', array( 'landing_page' ), 'radio' );
}

Doing the Rewrites

Then let's create a function:

/**
* Does the landing-pages rewrites.
*/
function do_landing_pages() {
// Require post types file.
require_once 'landing-pages.php';
// Do the rewrite for landing pages.
try {
// Create the rewrite object connecting the post type and taxonomy.
$landing_page_campaign_type = new \WPS\Rewrite\PostTypeByTaxonomy( array(
'post_type' => 'landing_page',
'taxonomy' => 'campaign_type',
) );
// Set the order of the rewrite to `%post_type%/%term%`. Defaults to `%term%/%post_type%`.
$landing_page_campaign_type->set_order( [
'%term%',
] );
// Add the feed/embed rewrite URLs.
$landing_page_campaign_type->add_embed_rewrites();
$landing_page_campaign_type->add_feed_rewrites();
} catch ( \Exception $e ) {
// do nothing right now.
// @todo Maybe do something.
}
}
// Do it!
do_landing_pages();

That's it!

On Activation

Because this is a plugin, we need to flush the rewrite rules when the plugin is activated. So in the plugin file (post-type-taxonomy-rewrite.php), let's flush the rules.

register_activation_hook( __FILE__, '\WPS\Plugins\Rewrite\PostTypeTaxonomy\on_activation' );
/**
* Flush rules on activation.
*/
function on_activation() {
// Register Landing Pages.
register_cpt_landing_pages();
register_tax_campaign_type();
// Flush the rules.
flush_rewrite_rules();
}

Prefix / Custom Post Type Example

What if you have a post type that you wanted to add a prefix slug, the date archives, and so use with this pattern (domain.com/{prefix}/{post-type-slug}/{postname}):

Setup

Now, create a videos.php file to contain our post type and taxonomy registration code for video (this replaces resources.php or landing-pages.php).

<?php
/**
* Post Type and Taxonomy Registration.
*
* Video Post Type.
*
* @package WPS\Plugins\Rewrite
* @author Travis Smith <t@wpsmith.net>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;
add_action( 'init', '\WPS\Plugins\Rewrite\PostTypeTaxonomy\register_cpt_videos' );
/**
* Register the Video Post Type.
*/
function register_cpt_videos() {
$labels = array(
'name' => __( 'Videos', 'wps' ),
'singular_name' => __( 'Video', 'wps' ),
);
$args = array(
'label' => __( 'Videos', 'wps' ),
'labels' => $labels,
'description' => '',
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'delete_with_user' => false,
'show_in_rest' => true,
'rest_base' => '',
'rest_controller_class' => 'WP_REST_Posts_Controller',
'has_archive' => 'videos',
'show_in_menu' => true,
'show_in_nav_menus' => true,
'exclude_from_search' => true,
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => array( 'slug' => 'video', 'with_front' => true ),
'query_var' => true,
'menu_position' => 5,
'supports' => array( 'title', 'editor', 'thumbnail' ),
'menu_icon' => 'dashicons-video-alt3',
);
register_post_type( 'video', $args );
}
view raw videos.php hosted with ❤ by GitHub

So now we should have this:

post-type-taxonomy-rewrite/
|- composer.json
|- post-type-taxonomy-rewrite.php
|- videos.php
|- vendor/

Doing the Rewrites

Then let's create a function:

/**
* Does the videos rewrites.
*/
function do_videos() {
// Require post types file.
require_once 'videos.php';
// Do the rewrite for landing pages.
try {
// Create the rewrite object connecting the post type and taxonomy.
$videos = new \WPS\Rewrite\PostTypeRewrite( array(
'post_type' => 'video',
) );
// Add all the rewrites. This includes the main, embed, feed, pagination, and date URLs.
$videos->add_all_rewrites();
} catch ( \Exception $e ) {
// do nothing right now.
// @todo Maybe do something.
}
}
// Do it!
do_videos();

That's it!

On Activation

Because this is a plugin, we need to flush the rewrite rules when the plugin is activated. So in the plugin file (post-type-taxonomy-rewrite.php), let's flush the rules.

register_activation_hook( __FILE__, '\WPS\Plugins\Rewrite\PostTypeTaxonomy\on_activation' );
/**
* Flush rules on activation.
*/
function on_activation() {
// Register Videos.
register_cpt_videos();
// Flush the rules.
flush_rewrite_rules();
}

Wrap-Up

If this was helpful, you can find all the code either in the gist or the Github repo (https://github.com/wpsmith/post-type-taxonomy-rewrite). Please feel free to let me know if there is anything I missed!