Using same slug for custom post type and custom taxonomy - React to WordPress
Same slug from custom post type and custom taxonomy

Using same slug for custom post type and custom taxonomy

It is fairly common that you, as a website manager, would like to use the same slug for custom post type as for custom taxonomy, but it is not a simple thing to do if you are not familiar with WordPress permalink structures.

I will explain, say your archive page is `` – that is the page that will hold all of your posts under the `web-tutorials` custom post type. Now you want your web-tutorial(a singular post) URL to be

web-tutorials Will be your custom post type slug.

taxonomy-category Your taxonomy category.

post-name Will be your post name/title.

It is impossible to do so without coding customizations, and to my surprise, it is a simple one.

Let us start by creating  a custom post type with the id of custom-post-type-name:

function register_post_type() {
		$labels = array(
			"name"          => __( 'Web Tutorials', 'textdomain' ),
			"singular_name" => __( 'Web Tutorial', 'textdomain' ),

		$args = array(
			"label"               => __( 'Web Tutorials', 'textdomain' ),
			"labels"              => $labels,
			"description"         => "",
			"public"              => true,
			"publicly_queryable"  => true,
			"show_ui"             => true,
			"show_in_rest"        => false,
			"rest_base"           => "",
			"show_in_menu"        => true,
                        "has_archive"         => 'web-tutorials',
			"exclude_from_search" => false,
			"capability_type"     => "post",
			'capabilities' => array(
				'read_post' => 'read_post'
			"map_meta_cap"        => true,
			"hierarchical"        => false,
			"rewrite"             => array( "slug" => "web-tutorials/%custom-taxonomy-name%", "with_front" => true ),
			"query_var"           => true,
			"supports"            => array( "title", "editor", "thumbnail" ),

		register_post_type( 'custom-post-type-name', $args );

In line 17 I’m creating the slug for our custom post archive page (web-tutorials), and in line 25, I’m creating the default slug for any post that is under the custom-post-type-name post type. Very easy to overlook but it is crucial for it to succeed, is to use in between the % the exact taxonomy name, in our case it is custom-taxonomy-name.

Let’s continue with creating a custom taxonomy with the previously mentioned id, custom-taxonomy-name:

function register_taxonomy() {
		$labels = array(
			"name"          => __( 'Taxonomy Categories', 'textdomain' ),
			"singular_name" => __( 'Taxonomy Category', 'textdomain' ),

		$args = array(
			"label"              => __( 'Taxonomy Category', 'textdomain' ),
			"labels"             => $labels,
			"public"             => true,
			"hierarchical"       => true,
			"show_ui"            => true,
			"show_in_menu"       => true,
			"show_in_nav_menus"  => true,
			"query_var"          => true,
			"rewrite"            => array( 'slug' => 'web-tutorials' ),
			"show_admin_column"  => false,
			"show_in_rest"       => false,
			"rest_base"          => "",
			"show_in_quick_edit" => false,

        register_taxonomy( 'custom-taxonomy-name', array( 'custon-post-type-name' ), $args );

As I’m finishing creating the taxonomy, our little project is almost done. Now, the last thing that is left is to change all of our permalinks to the correct URL structure. We will do it by hooking to post_type_link filter:

add_filter('post_type_link', 'cj_update_permalink_structure', 10, 2);
function cj_update_permalink_structure( $post_link, $post )
    if ( false !== strpos( $post_link, '%custom-taxonomy-name%' ) ) {
        $taxonomy_terms = get_the_terms( $post->ID, 'custom-taxonomy-name' ); (?)
        foreach ( $taxonomy_terms as $term ) { 
            if ( ! $term->parent ) {
                $post_link = str_replace( '%custom-taxonomy-name%', $term->slug, $post_link );
    return $post_link;

The filter gives us two variables; the first one is the `$post_link` and second is the $post data itself.

I’m checking if the $post_link contains %custom-taxonomy-name%, if it does, I’m getting all the terms for that specific post id with the taxonomy `custom-taxonomy-name` and rotating through each of them until I get the parent. Next, I’m adjusting the $post_link with the term’s slug.

Lastly, I’m returning the $post_link back to the filter.

That’s it. Now we have the exact URL structure we wanted to achieve, fast and easy.


7 comments on “Using same slug for custom post type and custom taxonomy”

    1. Sorry, Just seen the notification about the comment.

      Can you share your code please? are you adjusting

         "has_archive"         => 'web-tutorials',

      as well?

      1. My code varies slightly to yours but as far as I can see I added all the necessary elements:

        ‘singular_name’ => ‘Songs’,
        ‘add_new’ => ‘Add New’,
        ‘add_new_item’ => ‘Add New Song’,
        ‘edit_item’ => ‘Edit Song’,
        ‘new_item’ => ‘New Song’,
        ‘all_items’ => ‘All Songs’,
        ‘view_item’ => ‘View Song’,
        ‘search_items’ => ‘Search Songs’,
        ‘not_found’ => ‘No Songs Found’,
        ‘not_found_in_trash’ => ‘No Songs found in Trash’,
        ‘parent_item_colon’ => ”,
        ‘menu_name’ => ‘Songs’,

        $args = array(
        ‘labels’ => $labels,
        ‘public’ => true,
        ‘has_archive’ => ‘music’,
        ‘show_ui’ => true,
        ‘capability_type’ => ‘post’,
        ‘hierarchical’ => true,
        ‘query_var’ => true,
        ‘show_in_menu’ => true,
        ‘menu_icon’ => ‘dashicons-format-audio’,
        ‘taxonomies’ => array( ‘song_categories’),
        ‘taxonomies’ => array(‘songs’, ‘post_tag’),
        ‘rewrite’ => array(
        ‘slug’ => ‘music/%song_categories%’,
        ‘with_front’ => false,
        ‘supports’ => array(
        register_post_type( ‘songs’, $args );

        ‘song_categories’, ‘songs’,
        ‘hierarchical’ => true,
        ‘label’ => ‘Categories’,
        ‘query_var’ => true,
        ‘show_ui’ => true,
        ‘show_tagcloud’ => false,
        ‘rewrite’ => array(
        ‘slug’ => ‘music’,
        ‘with_front’ => false,

        add_action( ‘init’, ‘songs_init’ );

    1. Actually I spoke too soon. Whenever I do this all other permalinks except for the custom post type break on the site. This happens as soon as I add taxonomy as a property to the slug: %custom-taxonomy-name%

      For whatever reason, adding taxonomy to a slug breaks all permalinks even after flushing them.

        1. My code differs slightly from yours in that I’m trying to make the slug be only my taxonomy but other than that, it works the same way. It works but all other pages are broken on my site unless I remove %types_video%. For some reason including that in my video post type breaks all other post types regardless if I have a function that uses it or not. I think I’m getting the same problem as

          Here’s my code:

          add_action(‘init’, ‘create_post_type_video’);
          function create_post_type_video() {
          // Videos
          ‘labels’ => array(
          ‘name’ => ‘Videos’,
          ‘singular_name’ => ‘Video’,
          ‘add_new_item’ => ‘Add New Video’,
          ‘edit_item’ => ‘Edit Video’,
          ‘new_item’ => ‘New Video’,
          ‘view_item’ => ‘View Video’,
          ‘view_items’ => ‘View Videos’,
          ‘search_items’ => ‘Search Videos’,
          ‘not_found’ => ‘No Videos found.’,
          ‘not_found_in_trash’ => ‘No Videos found in Trash.’,
          ‘menu_name’ => ‘Videos’,
          ‘name_admin_bar’ => ‘Video’,
          ‘parent_item_colon’ => ‘Parent Video:’,
          ‘all_items’ => ‘All Videos’,
          ‘archives’ => ‘Video Archives’,
          ‘attributes’ => ‘Videos Attributes’,
          ‘insert_into_item’ => ‘Insert into Video’,
          ‘uploaded_to_this_item’ => ‘Uploaded to this Video.’,
          ‘filter_items_list’ => ‘Filter Videos list’,
          ‘items_list_navigation’ => ‘Videos list navigation’,
          ‘items_list’ => ‘Videos list’
          ‘supports’ => array(‘title’, ‘author’, ‘editor’, ‘thumbnail’),
          ‘taxonomies’ => array(‘types_video’, ‘category’),
          ‘public’ => true,
          ‘menu_position’ => 5,
          ‘menu_icon’ => ‘dashicons-format-video’,
          ‘rewrite’ => array(
          ‘slug’ => ‘%types_video%’

          // Types (Videos)
          $labels_video_types = array(
          ‘name’ => ‘Video Types’,
          ‘singular_name’ => ‘Video Type’,
          ‘menu_name’ => ‘Video Types’,
          ‘all_items’ => ‘All Video Types’,
          ‘edit_item’ => ‘Edit Video Type’,
          ‘view_item’ => ‘View Video Type’,
          ‘update_item’ => ‘Update Video Type’,
          ‘add_new_item’ => ‘Add New Video Type’,
          ‘new_item_name’ => ‘New Video Type Name’,
          ‘parent_item’ => ‘Parent Video Type’,
          ‘parent_item_colon’ => ‘Parent Video Type:’,
          ‘search_items’ => ‘Search Video Types’,
          ‘popular_items’ => ‘Popular Video Types’,
          ‘separate_items_with_commas’ => ‘Separate Video Types with commas.’,
          ‘add_or_remove_items’ => ‘Add or remove Video Types.’,
          ‘choose_from_most_used’ => ‘Choose from the most used Video Types.’,
          ‘not_found’ => ‘No Video Types found.’

          ‘labels’ => $labels_video_types,
          ‘rewrite’ => array(
          ‘slug’ => ‘video-types’
          ‘hierarchical’ => true,
          ‘show_ui’ => true,
          ‘show_admin_column’ => true,
          ‘query_var’ => true

          function update_permalinks($post_link, $post) {
          if (is_object($post) && $post->post_type == ‘video’) {
          $terms = wp_get_object_terms($post->ID, ‘types_video’);
          if ($terms) {
          return str_replace(‘%types_video%’ , $terms[0]->slug , $post_link);
          return $post_link;
          add_filter(‘post_type_link’, ‘update_permalinks’, 1, 2);

Leave a Reply

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