Custom Post Type Category URL

Option 1: Enable the post type archives and use a post type archive template.

  1. Enable the archives; e.g. when registering the post type using register_post_type():

    register_post_type( 'shop', [
        'has_archive' => 'shop', // the archives are accessible at example.com/shop
        // ...
    ] );
    
  2. Then create a custom archive template just for that post type; i.e. archive-shop.php.

  3. And you can use add_rewrite_rule() to rewrite example.com/shop/category/<category slug> to example.com/?category_name=<category slug>&post_type=shop:

    // This should be called in `init`.
    add_rewrite_rule(
        '^shop/category/([^/]+)/?$',
        'index.php?category_name=$matches[1]&post_type=shop',
        'top'
    );
    
  4. You’d need to use the pre_get_posts hook to filter the main WordPress query (since I could see you’re using some custom query vars/values). But in the archive template, you wouldn’t need the custom WP_Query query (i.e. $shop = new WP_Query).

Option 2: If you need to use a custom Page which displays posts in the custom post type.

And by “custom Page”, I’m referring to a post having the type page. Also, the advantage of using this option is that you don’t need to enable the default post type archives.

  1. So firstly, get the ID of the page which is using the custom template (template-shop.php).

  2. You’d use add_rewrite_rule() to rewrite example.com/shop/category/<category slug> to example.com/?shop_cat=<category slug>&page_id=<page ID>“shop_cat” can be changed to other non-reserved name, but you’ll also need to change the name in step #3 & #4 below:

    // This should be called in `init`.
    add_rewrite_rule(
        '^shop/category/([^/]+)/?$',
        // Make sure to use the correct Page ID.
        'index.php?shop_cat=$matches[1]&page_id=2',
        'top'
    );
    
  3. You’d use the query_vars hook to add the shop_cat variable to the public WordPress query vars:

    add_filter( 'query_vars', function ( $vars ) {
        $vars[] = 'shop_cat';
        return $vars;
    } );
    
  4. In template-shop.php or wherever necessary, just call get_query_var( 'shop_cat' ) to retrieve the category slug from the URL:

    $args = array(
        'post_type'      => array( 'shop' ),
        'order'          => 'ASC',
        'posts_per_page' => -1,
    );
    
    if ( $category = get_query_var( 'shop_cat' ) ) {
        $args['category_name'] = $category;
    }
    
    $shop = new WP_Query( $args );
    

Last but not least, there are other variations to both the above options, but the ones I provided should help you get started.