8

I have a custom post type products and a custom taxonomy 'Product Categories' with the slug products.

My goal: I'm trying to create a URL structure like so:

Product Post Type Archive: products/

Product Top-level Category Archives: products/product-category

Product Category Archives: products/product-category/product-sub-category

Product Page: products/product-category/product-sub-category/product-page

The problem: Product pages seem to only want to use actual categories. So URL structure is looking something like products/uncategorized/product-page. Category archive pages won't maintain the cpt base. So they turn out something like category/sub-category.

What I've tried: Innumerous google searches, quite a few snippets (the ones I remember I'll include below), a few custom functions, and a few plugins. No avail.

I have the following code.

add_action( 'init', 'products_cpt', 0);
add_action( 'init', 'register_taxonomies', 0 );

//A custom post type for all products
function products_cpt(){
    $labels = array(
        'name'               => _x('Products', 'Post Type General Name'),
        'singular_name'      => _x('Product', 'Post Type Singluar Name'),
        'menu_name'          => __('Products'),
        'parent_item_colon'  => __('Parent Product'),
        'all_items'          => __('All Products'),
        'view_item'          => __('View Product'),
        'add_new_item'       => __('Add New Product'),
        'add_new'            => __('Add New'),
        'edit_item'          => __('Edit Product'),
        'update_item'        => __('Update Product'),
        'search_items'       => __('Search Products'),
        'not_found'          => __('Not Found'),
        'not_found_in_trash' => __('Not Found in Trash')
    );
    $supports = array( 
        'title', 
        'editor', 
        'excerpt', 
        'thumbnail', 
        'custom-fields', 
        'revisions' 
    );
    $args = array(
        'labels'              => $labels,
        'hierarchical'        => true,
        'public'              => true,
        'exclude_from_search' => false,
        'show_in_admin_bar'   => true,
        'show_in_nav_menus'   => true,
        'publicly_queryable'  => true,
        'query_var'           => true,
        'taxonomies'          => array( 'chemicals' ),
        'supports'            => $supports,
        'has_archive'         => 'products'
    );
    register_post_type('products', $args);
}

function register_taxonomies() {
    $taxonomies = array(
        'products' => array(
            'Product',
            'Products'
        ),
    );
    foreach($taxonomies as $slug => $name){
        create_product_taxonomy($slug,$name[0],$name[1]);
    }
}

function create_product_taxonomy($slug, $singular, $plural) {
    $labels = array(
        'name'                       => _x( $singular.' Categories', 'Taxonomy General Name', 'text_domain' ),
        'singular_name'              => _x( $singular.' Category', 'Taxonomy Singular Name', 'text_domain' ),
        'menu_name'                  => __( $singular.' Categories', 'text_domain' ),
        'all_items'                  => __( 'All '.$singular.' Categories', 'text_domain' ),
        'parent_item'                => __( 'Parent '.$singular.' Category', 'text_domain' ),
        'parent_item_colon'          => __( 'Parent '.$singular.' Category:', 'text_domain' ),
        'new_item_name'              => __( 'New '.$singular.' Category Name', 'text_domain' ),
        'add_new_item'               => __( 'Add New '.$singular.' Category', 'text_domain' ),
        'edit_item'                  => __( 'Edit '.$singular.' Category', 'text_domain' ),
        'update_item'                => __( 'Update '.$singular.' Category', 'text_domain' ),
        'view_item'                  => __( 'View '.$singular.' Category', 'text_domain' ),
        'separate_items_with_commas' => __( 'Separate '.$singular.' Categories with commas', 'text_domain' ),
        'add_or_remove_items'        => __( 'Add or remove '.$singular.' Categories', 'text_domain' ),
        'choose_from_most_used'      => __( 'Choose from the most used '.$singular.' Categories', 'text_domain' ),
        'popular_items'              => __( 'Popular '.$singular.' Categories', 'text_domain' ),
        'search_items'               => __( 'Search '.$singular.' Categories', 'text_domain' ),
        'not_found'                  => __( 'Not Found', 'text_domain' ),
        'no_terms'                   => __( 'No '.$singular.' Categories', 'text_domain' ),
        'items_list'                 => __( $singular.' Categories list', 'text_domain' ),
        'items_list_navigation'      => __( $singular.' Categories list navigation', 'text_domain' ),
    );
     $args = array(
        'labels'                     => $labels,
        'hierarchical'               => true,
        'public'                     => true,
        'show_ui'                    => true,
        'show_admin_column'          => true,
        'show_in_nav_menus'          => true,
        'show_tagcloud'              => true,
        'has_archive'                => $plural,
        'rewrite' => array( 
            'slug'         => 'products',
            'with_front'   => true,
            'hierarchical' => true
        )
    );
    register_taxonomy( $slug, 'products', $args );
}

First things first, I set the permalinks setting to the following: Permalink Settings

Next, I tried adding the following code, however it only worked for product pages. Product Category Archives still 404'd.

add_filter( 'post_type_link', 'products_post_link', 1, 3 );
function products_post_link( $post_link, $id = 0 ){
    $post = get_post($id);  
    if ( is_object( $post ) ){
        $terms = wp_get_object_terms( $post->ID, 'products_category' );
        $slug_url = '';
        $last_id = 0;
        if( $terms ){
            foreach($terms as $term) {
                if ($term === reset($terms)){
                    foreach($terms as $termInner){
                        if($termInner->term_id == 0){
                            $slug_url .= $termInner->slug.'/';
                        }
                    }
                }elseif ($term === end($terms)){
                    foreach($terms as $termInner){
                        if($termInner->parent == $last_id){
                            $slug_url .= $termInner->slug;
                            $last_id = $termInner->term_id;
                        }
                    }
                }else{
                    foreach($terms as $termInner){
                        if($termInner->parent == $last_id){
                            $slug_url .= $termInner->slug.'/';
                            $last_id = $termInner->term_id;
                        }
                    }
                }
            }
            return str_replace( '%category%' , $slug_url , $post_link );
        }
    }
    return $post_link;  
}

And this to the CPT init function:

    'rewrite' => array(
        'slug'         => 'products/%category%',
        'with_front'   => false,
        'hierarchical' => true,
    ),

I also tried the following, don't remember what happened but I remember it didn't work:

add_action('init', 'custom_resource_rewrite_rules');
function custom_resource_rewrite_rules() {
    add_rewrite_rule('products/([A-Za-z0-9\-\_]+)/?', '$matches[1]', 'top');
}

I also tried playing with the term_link filter. No bueno.

Lastly, I tried the following Plugins, also no luck.

Custom Permalinks

Permalinks Customizer

Custom Post Type Permalinks

Does anyone have a solution to this? I'm pulling my hair out.

2
  • Could you ever have products/product-category/product-page? Feb 15, 2018 at 2:51
  • @MikeSchinkel I suppose, if the only category selected was a category with no parent categories
    – J Robz
    Feb 17, 2018 at 22:21

2 Answers 2

7

After forever, I figured out an answer!

First: we register the custom post type & custom taxonomy:

add_action( 'init', 'register_sps_products_post_type' );
function register_sps_products_post_type() {
    register_post_type( 'sps-product',
        array(
            'labels' => array(
                'name' => 'Products',
                'menu_name' => 'Product Manager',
                'singular_name' => 'Product',
                'all_items' => 'All Products'
            ),
            'public' => true,
            'publicly_queryable' => true,
            'show_ui' => true,
            'show_in_menu' => true,
            'show_in_nav_menus' => true,
            'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'comments', 'post-formats', 'revisions' ),
            'hierarchical' => false,
            'has_archive' => 'products',
            'taxonomies' => array('product-category'),
            'rewrite' => array( 'slug' => 'products/%product_category%', 'hierarchical' => true, 'with_front' => false )
        )
    );
    register_taxonomy( 'product-category', array( 'sps-product' ),
        array(
            'labels' => array(
                'name' => 'Product Categories',
                'menu_name' => 'Product Categories',
                'singular_name' => 'Product Category',
                'all_items' => 'All Categories'
            ),
            'public' => true,
            'hierarchical' => true,
            'show_ui' => true,
            'rewrite' => array( 'slug' => 'products', 'hierarchical' => true, 'with_front' => false ),
        )
    );
}

Next, we add a new rewrite rule so Wordpress knows how to interpret our new permalink structure:

add_action( 'generate_rewrite_rules', 'register_product_rewrite_rules' );
function register_product_rewrite_rules( $wp_rewrite ) {
    $new_rules = array( 
        'products/([^/]+)/?$' => 'index.php?product-category=' . $wp_rewrite->preg_index( 1 ), // 'products/any-character/'
        'products/([^/]+)/([^/]+)/?$' => 'index.php?post_type=sps-product&product-category=' . $wp_rewrite->preg_index( 1 ) . '&sps-product=' . $wp_rewrite->preg_index( 2 ), // 'products/any-character/post-slug/'
        'products/([^/]+)/([^/]+)/page/(\d{1,})/?$' => 'index.php?post_type=sps-product&product-category=' . $wp_rewrite->preg_index( 1 ) . '&paged=' . $wp_rewrite->preg_index( 3 ), // match paginated results for a sub-category archive
        'products/([^/]+)/([^/]+)/([^/]+)/?$' => 'index.php?post_type=sps-product&product-category=' . $wp_rewrite->preg_index( 2 ) . '&sps-product=' . $wp_rewrite->preg_index( 3 ), // 'products/any-character/sub-category/post-slug/'
        'products/([^/]+)/([^/]+)/([^/]+)/([^/]+)/?$' => 'index.php?post_type=sps-product&product-category=' . $wp_rewrite->preg_index( 3 ) . '&sps-product=' . $wp_rewrite->preg_index( 4 ), // 'products/any-character/sub-category/sub-sub-category/post-slug/'
    );
    $wp_rewrite->rules = $new_rules + $wp_rewrite->rules;
}

The next function on the other hand fixes the issue with the sub-categories. The issue itself consists in the fact, that when you try to load a page with URL faq/category/child-category/, WordPress would try to load a post with slug “child-category” instead of the sub-category with slug “child-category”.

// A hacky way of adding support for flexible custom permalinks
// There is one case in which the rewrite rules from register_kb_rewrite_rules() fail:
// When you visit the archive page for a child section(for example: http://example.com/faq/category/child-category)
// The deal is that in this situation, the URL is parsed as a Knowledgebase post with slug "child-category" from the "category" section
function fix_product_subcategory_query($query) {
    if ( isset( $query['post_type'] ) && 'sps-product' == $query['post_type'] ) {
        if ( isset( $query['sps-product'] ) && $query['sps-product'] && isset( $query['product-category'] ) && $query['product-category'] ) {
            $query_old = $query;
            // Check if this is a paginated result(like search results)
            if ( 'page' == $query['product-category'] ) {
                $query['paged'] = $query['name'];
                unset( $query['product-category'], $query['name'], $query['sps-product'] );
            }
            // Make it easier on the DB
            $query['fields'] = 'ids';
            $query['posts_per_page'] = 1;
            // See if we have results or not
            $_query = new WP_Query( $query );
            if ( ! $_query->posts ) {
                $query = array( 'product-category' => $query['sps-product'] );
                if ( isset( $query_old['product-category'] ) && 'page' == $query_old['product-category'] ) {
                    $query['paged'] = $query_old['name'];
                }
            }
        }
    }
    return $query;
}
add_filter( 'request', 'fix_product_subcategory_query', 10 );

This function lets WordPress know how to handle %product_category% in your custom post type rewrite slug structure:

function filter_post_type_link($link, $post)
{
    if ($post->post_type != 'sps-product')
        return $link;

    if ($cats = get_the_terms($post->ID, 'product-category'))
    {
        $link = str_replace('%product_category%', get_taxonomy_parents(array_pop($cats)->term_id, 'product-category', false, '/', true), $link); // see custom function defined below\
        $link = str_replace('//', '/', $link);
        $link = str_replace('http:/', 'http://', $link);
    }
    return $link;
}
add_filter('post_type_link', 'filter_post_type_link', 10, 2);

A custom function based off of get_category_parents. It gets the taxonomy's parents:

// my own function to do what get_category_parents does for other taxonomies
function get_taxonomy_parents($id, $taxonomy, $link = false, $separator = '/', $nicename = false, $visited = array()) {    
    $chain = '';   
    $parent = &get_term($id, $taxonomy);

    if (is_wp_error($parent)) {
        return $parent;
    }

    if ($nicename)    
        $name = $parent -> slug;        
else    
        $name = $parent -> name;

    if ($parent -> parent && ($parent -> parent != $parent -> term_id) && !in_array($parent -> parent, $visited)) {    
        $visited[] = $parent -> parent;    
        $chain .= get_taxonomy_parents($parent -> parent, $taxonomy, $link, $separator, $nicename, $visited);

    }

    if ($link) {
        // nothing, can't get this working :(
    } else    
        $chain .= $name . $separator;    
    return $chain;    
}

Sources:

2
  • This code does seem to work but it causes a couple of issues when I implement it on my website. First, it causes Posts and Pages to break - only items under a custom post type actually work. Secondly, for some reason the code causes the single template for my custom post type to be ignored entirely. I've been trying to figure out the cause, but no joy yet! Sep 24, 2019 at 19:04
  • I've resolved the first issue, but with regard to the second issue, the fix_product_subcategory_query() function breaks the single template for my custom post type. Specifically, it just loads the generic "single.php" template and not the one for my CPT. Can you offer any resolution? Sep 25, 2019 at 0:25
2

Actually, this is a horrible way of doing this.

If you add the taxonomy before the custom post type using the timing of the add_action, you'll be able to use the custom post type rewrite rule to prefix the taxonomy.

ex.

    
    // Register Custom Taxonomy
    function type_taxonomy() {
    
        $labels = array(
            'name'                       => _x( 'Types', 'Taxonomy General Name', 'asw' ),
            'singular_name'              => _x( 'Type', 'Taxonomy Singular Name', 'asw' ),
        );
        $rewrite = array(
            'slug'                       => 'update/type',
            'with_front'                 => false,
            'hierarchical'               => false,
        );
        $args = array(
            'labels'                     => $labels,
            'hierarchical'               => true,
            'public'                     => true,
            'show_ui'                    => true,
            'show_admin_column'          => true,
            'show_in_nav_menus'          => true,
            'show_tagcloud'              => true,
            'rewrite'                    => $rewrite,
            'show_in_rest'               => true,
        );
        register_taxonomy( 'type', array( 'updates' ), $args );
    
    }
// Set the timing before the post type
    add_action( 'init', 'type_taxonomy', $timing = 0 );
    
    }
    
    if ( ! function_exists('updates_post_type') ) {
    
    // Register Custom Post Type
    function updates_post_type() {
    
        $labels = array(
            'name'                  => _x( 'Updates', 'Post Type General Name', 'asw' ),
            'singular_name'         => _x( 'Updates', 'Post Type Singular Name', 'asw' ),
        );
        $rewrite = array(
            'slug'                  => 'update',
            'with_front'            => true,
            'pages'                 => true,
            'feeds'                 => true,
        );
        $args = array(
            'label'                 => __( 'Updates', 'asw' ),
            'description'           => __( 'Posts should include important updates on health, well being and community', 'asw' ),
            'labels'                => $labels,
            'supports'              => array( 'title', 'editor', 'thumbnail' ),
            'taxonomies'            => array( 'type', 'category', 'post_tag', 'brands', 'markets' ),
            'hierarchical'          => false,
            'public'                => true,
            'show_ui'               => true,
            'show_in_menu'          => true,
            'menu_position'         => 5,
            'menu_icon'             => 'dashicons-heart',
            'show_in_admin_bar'     => true,
            'show_in_nav_menus'     => true,
            'can_export'            => true,
            'has_archive'           => true,
            'exclude_from_search'   => false,
            'publicly_queryable'    => true,
            'rewrite'               => $rewrite,
            'capability_type'       => 'page',
            'show_in_rest'          => true,
        );
        register_post_type( 'updates', $args );
    
    }
// Set the timing after the taxonomy 
    add_action( 'init', 'updates_post_type', $timing = 10 );
    
    }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.