Custom Taxonomy in Custom Post Type URL

Let’s just say that you want to register one custom post type Product and a corresponding custom taxonomy Product Category.

You want the URL structures to be like this:
http://www.test.com/product/a-category/ —> display a-category archive page.
http://www.test.com/product/a-category/this-is-a-product-page/ —> display this-is-a-product-page

First, you need to register the custom post type and taxonomy the right way:


register_taxonomy( 'product_category', array( 'product' ), array('public'=>true ,'rewrite'=>array('slug'=>'product')) );

register_post_type( 'product', array('public'=>true) );


Obviously, I skip most of arguments but just use 2 of them which are the key to the desired URL structures.

The public is self explanatory, meaning your product and product category can be visited publicly.

The ‘rewrite’=>array(‘slug’=>’product’) will take care of “http://www.test.com/product/a-category/” URL structure.

Okay, right now, we just need to find out how to achieve the second URL structure.

To use custom URL structure, WordPress provides a way to do this: add_rewrite_rule

add_rewrite_rule("^product/([^/]+)/([^/]+)/?",'index.php?post_type=product&product_category=$matches[1]&product=$matches[2]','top');

When and where do we call this function?

Well, I usually do it in the plugin activation.

add_action( 'admin_init', array( $this, 'install' ) );
function install(){
     if($this->is_new_version()){
         add_rewrite_rule("^product/([^/]+)/([^/]+)/?",'index.php?post_type=product&product_category=$matches[1]&product=$matches[2]','top');
         flush_rewrite_rules();
     }
}

Please note that you gotta flush rewrite rules to let the new rewrite rule take place.

I’ve seen many people, including me, add_rewrite_rule in init hook. But from what I found in the wp-include/rewrite.php, those rewrite rules are simply stored in a variable called “extra_rules”, which won’t be merged into “rules” until rewrite rules are flushed. “rules” is the one will be used for URL parsing in class-wp.php. Feel free to correct me if I’m wrong.

Right now, you can visit your product page like this: http://www.test.com/product/a-category/this-is-a-product-page/

You may think we’ve achieve the desired URL structures but actually there is a problem.

The product category archive pagination page is 404. For example http://www.test.com/product/a-category/page/2/

Okay, the fix is simple, you just need to add another rewrite rule:

add_rewrite_rule(“^product/([^/]+)/page/(\d+)/?”,’index.php?post_type=product&product_category=$matches[1]&paged=$matches[2]’,’top’);

It’s worth noting that the order of how you add rewrite rules could affect the result.

If the order is like this

add_rewrite_rule("^product/([^/]+)/([^/]+)/?",'index.php?post_type=product&product_category=$matches[1]&product=$matches[2]','top');

//URL Structure: http://www.test.com/product/a-category/this-is-a-product-page/ 

add_rewrite_rule("^product/([^/]+)/page/(\d+)/?",'index.php?post_type=product&product_category=$matches[1]&paged=$matches[2]','top');

//URL structure http://www.test.com/product/a-category/page/2/

When you visit http://www.test.com/product/a-category/page/2/, you’ll actually get a 404 error instead. This is because WordPress thinks that you are visiting a product named “page”.

To debug such problem, you can always output global variable $wp where you’ll see some useful information about URL parsing.

Okay. there is still one problem we need to cope.

How to change the post links? The post links in the post table of the backend is still http://www.test.com/product/a-product-page

What you are gonna do is to hook to post_type_link and post_link to change the link.

add_filter( 'post_type_link', array($this,'post_type_link') , 10, 2 );
add_filter( 'post_link', array($this,'post_type_link') , 10, 2 );

function post_type_link($post_link, $id = 0 ){

       $post = get_post($id);
       if(!$post instanceof WP_Post)
	   return $post_link;
       $typenow = $post->post_type;
	if ( $post->post_type !== 'product' )
		return $post_link;


        $terms = get_the_terms($post->ID, 'product_category');

	if( is_wp_error($terms) || !$terms ) {
	    $category = 'uncategorized';
	}
	else {
	   $category_obj = array_pop($terms);
	   $category = $category_obj->slug;
	}

	return home_url(user_trailingslashit( "product/$category/$post->post_name" ));
}

As you can see, I use “uncategorized” for product who doesn’t have a category while pick up a category for post who have more than 1 category.

Share and Enjoy

    FacebookTwitterGoogle PlusLinkedInStumbleUponPinterestRedditTumblrDiggEmailPrint

Related Posts