WooCommerce Catalog Visibility: B2B Role-Based Product Filtering

WooCommerce’s catalog visibility system controls where products appear — in the shop, in search results, or only via direct URL. Understanding how to filter visibility programmatically lets you build role-based catalogs, B2B price lists, and pre-launch product pages without custom post status hacks.

Problem: A B2B WooCommerce site needs to hide certain products or entire categories from non-registered visitors or specific customer roles — wholesale products should not be visible to retail customers.

Solution: Use the woocommerce_product_query action to add a meta_query that filters products by a custom visibility meta field for the current user's role. For category-level restriction, override the WC_Product_Visibility logic with the woocommerce_product_is_visible filter and check current_user_can() against a custom capability.

The examples below filter catalog queries to hide products by category for guests, programmatically set product visibility, restrict specific products to logged-in users, and build a wholesale catalog that shows a different price tier.

set_catalog_visibility( $visibility );
    $product->save();
}

// Hide an entire category from guests (B2B catalog)
add_action( 'woocommerce_product_query', function( WP_Query $q ) {
    if ( is_user_logged_in() ) return;

    $tax_query = (array) $q->get( 'tax_query' );
    $tax_query[] = [
        'taxonomy' => 'product_cat',
        'field'    => 'slug',
        'terms'    => [ 'wholesale', 'trade-only' ],
        'operator' => 'NOT IN',
    ];
    $q->set( 'tax_query', $tax_query );
} );

// Filter the main shop query to exclude products tagged 'coming-soon' for guests
add_action( 'pre_get_posts', function( WP_Query $q ) {
    if ( ! $q->is_main_query() || ! is_shop() ) return;
    if ( current_user_can( 'manage_options' ) ) return;

    $tax_query = (array) $q->get( 'tax_query' );
    $tax_query[] = [
        'taxonomy' => 'product_tag',
        'field'    => 'slug',
        'terms'    => [ 'coming-soon' ],
        'operator' => 'NOT IN',
    ];
    $q->set( 'tax_query', $tax_query );
} );

Show different prices to wholesale users and restrict single product pages:

roles, true ) ) return $price;

    $wholesale = get_post_meta( $product->get_id(), '_wholesale_price', true );
    return $wholesale ?: $price;
}, 10, 2 );

// Also apply to variation prices
add_filter( 'woocommerce_product_variation_get_price', function( string $price, WC_Product_Variation $product ): string {
    if ( ! is_user_logged_in() ) return $price;

    $user = wp_get_current_user();
    if ( ! in_array( 'wholesale_customer', (array) $user->roles, true ) ) return $price;

    $wholesale = get_post_meta( $product->get_id(), '_wholesale_price', true );
    return $wholesale ?: $price;
}, 10, 2 );

// Redirect guests away from restricted single product pages
add_action( 'template_redirect', function() {
    if ( ! is_product() || is_user_logged_in() ) return;

    global $post;
    $restricted = get_post_meta( $post->ID, '_requires_login', true );
    if ( $restricted ) {
        wp_safe_redirect( wp_login_url( get_permalink() ) );
        exit;
    }
} );

// Add 'Requires Login' meta box in the product editor
add_action( 'woocommerce_product_options_general_product_data', function() {
    woocommerce_wp_checkbox( [
        'id'    => '_requires_login',
        'label' => __( 'Requires Login', 'myplugin' ),
        'desc_tip' => true,
        'description' => __( 'Redirect guests to login before viewing this product.', 'myplugin' ),
    ] );
} );

NOTE: The woocommerce_product_query action modifies the WC-specific product loop query, while pre_get_posts modifies the broader WordPress query — use both when you need catalog restrictions to apply to archive pages, shortcodes, and block-rendered product lists simultaneously.

Leave Comment

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