WP Smith

Creating WordPress & Genesis Websites Since 2010

  • Home
  • About
  • Services
  • Blog
  • Contact

Jan 05 2019

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:

  • Singular Page: https://domain.com/cpt-slug/post-name-slug/
  • Paginated Singular Pages: https://domain.com/cpt-slug/post-name-slug/2/
  • Archive Page: https://domain.com/cpt-archive-slug/
  • Paginated Archive Pages: https://domain.com/cpt-slug/post-name-slug/page/2/
  • Trackback Page: https://domain.com/cpt-slug/post-name-slug/trackback/
  • Feed Pages: https://domain.com/cpt-slug/post-name-slug/feed/rss/, https://domain.com/cpt-slug/post-name-slug/rss/
  • Comment Page: https://domain.com/cpt-slug/post-name-slug/comment-page/
  • Embed Page: https://domain.com/cpt-slug/post-name-slug/embed/
  • Attachment:
    • Attachment Page: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/
    • Attachment Trackback Page: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/trackback/
    • Attachment Feed Pages: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/feed/rss/ , https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/rss/
    • Attachment Comment Page: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/comment-page/
    • Attachment Embed Page: https://domain.com/cpt-slug/post-name-slug/attachment/my-attachment-slug/embed/

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

  • Term Archive Page: https://domain.com/custom-taxonomy-slug/term-slug/
  • Paginated Archive Pages: https://domain.com/custom-taxonomy-slug/term-slug/page/2/
  • Embed Page: https://domain.com/custom-taxonomy-slug/term-name-slug/embed/
  • Feed Pages: https://domain.com/custom-taxonomy-slug/term-name-slug/feed/rss/, https://domain.com/tax-slug/term-name-slug/rss/

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 <[email protected]>
* 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 <[email protected]>
* @license GPL-2.0+
* @link https://wpsmith.net/
*/
namespace WPS\Plugins\Rewrite\PostTypeTaxonomy;
view raw post-type-taxonomy-rewrite-resources.php hosted with ❤ by GitHub

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": "[email protected]"
}
],
"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';
view raw post-type-taxonomy-rewrite-videos.php hosted with ❤ by GitHub

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}):

  • Single Custom Post Type Post: https://domain.com/resource/tutorials/setting-up-something
  • Custom Taxonomy Archive: https://domain.com/resource/tutorials/
  • Custom Post Type Archive: https://domain.com/resource/

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 <[email protected]>
* @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();
view raw post-type-taxonomy-rewrite-resources.php hosted with ❤ by GitHub

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();
}
view raw post-type-taxonomy-rewrite-resources.php hosted with ❤ by GitHub

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}):

  • Single Parent Custom Post Type Post: https://domain.com/email/landing-page/selling-something
  • Single Child Custom Post Type Post: https://domain.com/email/landing-page/selling-parent/selling-something
  • Custom Taxonomy Archive: https://domain.com/email/

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 <[email protected]>
* @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' );
}
view raw landing-pages.php hosted with ❤ by GitHub

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": "[email protected]"
}
],
"minimum-stability": "dev",
"require": {
"wpsmith/single-term-taxonomy": "dev-master",
"wpsmith/rewrite": "dev-master"
}
}
view raw composer-landing-pages.json hosted with ❤ by GitHub

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' );
}
view raw landing-pages.php hosted with ❤ by GitHub

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();
view raw post-type-taxonomy-rewrite-landing-pages.php hosted with ❤ by GitHub

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();
}
view raw post-type-taxonomy-rewrite-landing-pages.php hosted with ❤ by GitHub

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}):

  • Single Custom Post Type Post: https://domain.com/video/some-music-video
  • Date Archives: https://domain.com/video/2019, https://domain.com/video/2019/01, and https://domain.com/video/2019/01/05
  • Custom Post Type Archive: https://domain.com/videos/

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 <[email protected]>
* @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();
view raw post-type-taxonomy-rewrite-videos.php hosted with ❤ by GitHub

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();
}
view raw post-type-taxonomy-rewrite-videos.php hosted with ❤ by GitHub

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!

Written by Travis Smith · Categorized: Tutorials

StudioPress Premium WordPress Themes     WP Engine Managed WordPress Hosting

What can I do for you!?

Custom Development

We develop plugins by determining both business/functional and technical requirements, following WordPress development best practices, and using agile methodology to ensure you get the best solution.

Consulting

Have questions? Need a reliable developer to consult? Please contact us today!

Customized Theme

We can customize your theme or child theme, or create a child theme for you based on your needs while enhancing the performance of every individual attribute.

Customized Plugin

We can customize your plugins, extend plugins (e.g., Gravity Forms, Jetpack, Soliloquy) based on your needs ensuring security, performance, and positive business impact.

Contact Us

About Travis Smith

As a WordPress enthusiast, developer, and speaker, Travis writes about what he learns in WordPress trying to help other WordPress travelers, beginners and enthusiasts with tutorials, explanations, & demonstrations.

Comments

  1. Jakob M says

    April 12, 2020 at 11:39 am

    Hello, thank you for sharing your library.

    When trying to use this, I discovered that the namespace examples in this tutorial don’t match the
    wpsmith/rewrite package namespace exactly:

    instead of using:
    \WPS\Rewrite\PostTypeByTaxonomy( )

    the right path was:
    \WPS\WP\Rewrite\PostTypeByTaxonomy( )

    Reply

Leave a Reply Cancel reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  • Twitter
  • Facebook
  • LinkedIn
  • Google+
  • RSS

Copyright © 2025 � WP Smith on Genesis on Genesis Framework � WordPress � Log in