Commit 51c3559a79b for woocommerce

commit 51c3559a79b339ba286b445ebb938c895b43f960
Author: Tung Du <dinhtungdu@gmail.com>
Date:   Thu Oct 22 11:44:18 2026 +0700

    Add filter support to local Product Collections (#69888)

    * feat: parse collection-scoped product filter parameters

    * feat: enable filters within local product collections

    * fix: preserve overlapping collection filter selections

    Amp-Thread-ID: https://ampcode.com/threads/T-01a14026-0de6-715a-b1e4-01dbc9c33451
    Co-authored-by: Amp <amp@ampcode.com>

    * chore: add changelog for local collection filters

    Amp-Thread-ID: https://ampcode.com/threads/T-01a14026-0de6-715a-b1e4-01dbc9c33451
    Co-authored-by: Amp <amp@ampcode.com>

    * fix: align local collection CI coverage and brand archive controls

    Amp-Thread-ID: https://ampcode.com/threads/T-01a14026-0de6-715a-b1e4-01dbc9c33451
    Co-authored-by: Amp <amp@ampcode.com>

    * fix: preserve collection context in product filters metadata

    Declare the context in block.json because metadata registration bypasses the PHP fallback declaration.

    Amp-Thread-ID: https://ampcode.com/threads/T-01a14026-0de6-715a-b1e4-01dbc9c33451
    Co-authored-by: Amp <amp@ampcode.com>

    * test: remove redundant filter test description

    Amp-Thread-ID: https://ampcode.com/threads/T-01a14026-0de6-715a-b1e4-01dbc9c33451
    Co-authored-by: Amp <amp@ampcode.com>

    ---------

    Co-authored-by: Amp <amp@ampcode.com>

diff --git a/plugins/woocommerce/changelog/add-local-product-collection-filters b/plugins/woocommerce/changelog/add-local-product-collection-filters
new file mode 100644
index 00000000000..441f3fa35a4
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-local-product-collection-filters
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Let shoppers filter individual product collections without changing other collections on the page.
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/test/block.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/test/block.ts
index 72702c5e3cd..faff51c7bdd 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/test/block.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/cart/inner-blocks/cart-cross-sells-block/test/block.ts
@@ -42,9 +42,7 @@ describe( 'createCrossSellsProductCollection transform function', () => {
 			...crossSells.attributes.query,
 			perPage: 3,
 		} );
-		expect( transformedBlock.attributes.hideControls ).toEqual(
-			crossSells.attributes.hideControls
-		);
+		expect( transformedBlock.attributes.hideControls ).toEqual( [] );
 	} );

 	it( 'creates inner blocks from cross-sells template', () => {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/classic-template/single-product.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/classic-template/single-product.ts
index 5a8dbb06867..f20f079834b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/classic-template/single-product.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/classic-template/single-product.ts
@@ -77,7 +77,6 @@ const getBlockifiedTemplate = () =>
 					perPage: 5,
 					pages: 1,
 					woocommerceStockStatus: [ 'instock', 'onbackorder' ],
-					filterable: false,
 				},
 				displayLayout: {
 					type: 'flex',
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/best-sellers.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/best-sellers.tsx
index 95cab5cd23d..60968954b92 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/best-sellers.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/best-sellers.tsx
@@ -36,7 +36,7 @@ const attributes = {
 		perPage: 5,
 		pages: 1,
 	},
-	hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
+	hideControls: [ CoreFilterNames.ORDER ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-brand.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-brand.tsx
index 2acabdfd5da..e64dabea661 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-brand.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-brand.tsx
@@ -31,7 +31,7 @@ const attributes = {
 		columns: 5,
 		shrinkColumns: true,
 	},
-	hideControls: [ CoreFilterNames.HAND_PICKED, CoreFilterNames.FILTERABLE ],
+	hideControls: [ CoreFilterNames.HAND_PICKED ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-category.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-category.tsx
index 5c36f36836f..eb3d3a98e97 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-category.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-category.tsx
@@ -34,7 +34,7 @@ const attributes = {
 		columns: 5,
 		shrinkColumns: true,
 	},
-	hideControls: [ CoreFilterNames.HAND_PICKED, CoreFilterNames.FILTERABLE ],
+	hideControls: [ CoreFilterNames.HAND_PICKED ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-tag.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-tag.tsx
index 612a0017c6d..f308d1aaa4b 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-tag.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/by-tag.tsx
@@ -31,7 +31,7 @@ const attributes = {
 		columns: 5,
 		shrinkColumns: true,
 	},
-	hideControls: [ CoreFilterNames.HAND_PICKED, CoreFilterNames.FILTERABLE ],
+	hideControls: [ CoreFilterNames.HAND_PICKED ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/cart-contents.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/cart-contents.tsx
index cb3a49c496a..1bb557519ed 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/cart-contents.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/cart-contents.tsx
@@ -50,7 +50,6 @@ const attributes = {
 		CoreFilterNames.STOCK_STATUS,
 		CoreFilterNames.HAND_PICKED,
 		CoreFilterNames.TAXONOMY,
-		CoreFilterNames.FILTERABLE,
 		CoreFilterNames.CREATED,
 		CoreFilterNames.PRICE_RANGE,
 	],
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/cross-sells.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/cross-sells.tsx
index 4014ab85d5a..a3495a0fe38 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/cross-sells.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/cross-sells.tsx
@@ -16,7 +16,7 @@ import {
 	DEFAULT_QUERY,
 	DEFAULT_ATTRIBUTES,
 } from '../constants';
-import { CoreCollectionNames, CoreFilterNames } from '../types';
+import { CoreCollectionNames } from '../types';

 const collection = {
 	name: CoreCollectionNames.CROSS_SELLS,
@@ -43,7 +43,6 @@ export const attributes = {
 		perPage: 8,
 		pages: 1,
 	},
-	hideControls: [ CoreFilterNames.FILTERABLE ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/featured.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/featured.tsx
index a1c7d52d406..a73fe841fba 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/featured.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/featured.tsx
@@ -34,7 +34,7 @@ const attributes = {
 		perPage: 5,
 		pages: 1,
 	},
-	hideControls: [ CoreFilterNames.FEATURED, CoreFilterNames.FILTERABLE ],
+	hideControls: [ CoreFilterNames.FEATURED ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/hand-picked.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/hand-picked.tsx
index 1b46c5fb727..6ccf5e3916c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/hand-picked.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/hand-picked.tsx
@@ -72,11 +72,7 @@ const attributes = {
 	query: {
 		orderBy: 'post__in',
 	},
-	hideControls: [
-		CoreFilterNames.HAND_PICKED,
-		CoreFilterNames.FILTERABLE,
-		CoreFilterNames.ORDER,
-	],
+	hideControls: [ CoreFilterNames.HAND_PICKED, CoreFilterNames.ORDER ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/new-arrivals.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/new-arrivals.tsx
index 5e2b84bc118..a14ad30f027 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/new-arrivals.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/new-arrivals.tsx
@@ -44,7 +44,7 @@ const attributes = {
 			value: '-7 days',
 		},
 	},
-	hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
+	hideControls: [ CoreFilterNames.ORDER ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/on-sale.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/on-sale.tsx
index c67f2ed84a0..ea8860dea9e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/on-sale.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/on-sale.tsx
@@ -37,7 +37,7 @@ const attributes = {
 		perPage: 5,
 		pages: 1,
 	},
-	hideControls: [ CoreFilterNames.ON_SALE, CoreFilterNames.FILTERABLE ],
+	hideControls: [ CoreFilterNames.ON_SALE ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/top-rated.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/top-rated.tsx
index 5bc4590abe2..b742913da50 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/top-rated.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/top-rated.tsx
@@ -38,7 +38,7 @@ const attributes = {
 		perPage: 5,
 		pages: 1,
 	},
-	hideControls: [ CoreFilterNames.ORDER, CoreFilterNames.FILTERABLE ],
+	hideControls: [ CoreFilterNames.ORDER ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/upsells.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/upsells.tsx
index 707aa51edfa..1bd8efdc75c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/upsells.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/collections/upsells.tsx
@@ -16,7 +16,7 @@ import {
 	DEFAULT_QUERY,
 	DEFAULT_ATTRIBUTES,
 } from '../constants';
-import { CoreCollectionNames, CoreFilterNames } from '../types';
+import { CoreCollectionNames } from '../types';

 const collection = {
 	name: CoreCollectionNames.UPSELLS,
@@ -43,7 +43,6 @@ const attributes = {
 		perPage: 8,
 		pages: 1,
 	},
-	hideControls: [ CoreFilterNames.FILTERABLE ],
 };

 const heading: InnerBlockTemplate = [
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/constants.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/constants.ts
index 8a0f856bc6b..6ba6060f014 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/constants.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/constants.ts
@@ -62,7 +62,6 @@ export const DEFAULT_QUERY: ProductCollectionQuery = {
 	woocommerceHandPickedProducts: [],
 	timeFrame: undefined,
 	priceRange: undefined,
-	filterable: false,
 	relatedBy: {
 		categories: true,
 		tags: true,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
index 9190fd49b0c..7ac6d944b35 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/index.tsx
@@ -25,10 +25,7 @@ import {
 } from '../../types';
 import { setQueryAttribute, getDefaultSettings } from '../../utils';
 import ColumnsControl from './columns-control';
-import {
-	InheritQueryControl,
-	FilterableControl,
-} from './use-page-context-control';
+import { InheritQueryControl } from './use-page-context-control';
 import useCarouselLayoutAdjustments from './use-carousel-layout-adjustments';
 import useEmailPaginationAdjustments from './use-email-pagination-adjustments';
 import useEmailColumnAdjustments from './use-email-column-adjustments';
@@ -93,8 +90,6 @@ const ProductCollectionInspectorControls = (
 	const showCustomQueryControls = inherit === false;
 	const showInheritQueryControl =
 		isArchiveTemplate && shouldShowFilter( CoreFilterNames.INHERIT );
-	const showFilterableControl =
-		! isArchiveTemplate && shouldShowFilter( CoreFilterNames.FILTERABLE );
 	const showCustomOrderControl =
 		showCustomQueryControls && shouldShowFilter( CoreFilterNames.ORDER );
 	const showDefaultOrderControl = ! showCustomQueryControls;
@@ -204,9 +199,6 @@ const ProductCollectionInspectorControls = (
 				{ showInheritQueryControl && (
 					<InheritQueryControl { ...queryControlProps } />
 				) }
-				{ showFilterableControl && (
-					<FilterableControl { ...queryControlProps } />
-				) }
 				{ showCustomOrderControl && (
 					<CustomQueryOrderByControl { ...queryControlProps } />
 				) }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-page-context-control.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-page-context-control.tsx
index 7053fe6e7e5..9f8a14a5e70 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-page-context-control.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/inspector-controls/use-page-context-control.tsx
@@ -25,25 +25,18 @@ import {
 	type QueryControlProps,
 } from '../../types';
 import { DEFAULT_QUERY } from '../../constants';
-import {
-	getDefaultValueOfInherit,
-	getDefaultValueOfFilterable,
-} from '../../utils';
+import { getDefaultValueOfInherit } from '../../utils';

 const label = __( 'Query type', 'woocommerce' );
 const defaultOptionLabel = __( 'Default', 'woocommerce' );
 const customOptionLabel = __( 'Custom', 'woocommerce' );

 const defaultInheritHelpText = __(
-	'Display products based on the current template and allow shoppers to filter.',
-	'woocommerce'
-);
-const defaultFilterableHelpText = __(
-	'Show products based on specific criteria and allow shoppers to filter.',
+	'Display products based on the current template.',
 	'woocommerce'
 );
 const customHelpText = __(
-	'Show a list of products based on fixed criteria.',
+	'Show products based on specific criteria.',
 	'woocommerce'
 );

@@ -114,53 +107,4 @@ const InheritQueryControl = ( {
 	);
 };

-const FilterableControl = ( {
-	setQueryAttribute,
-	trackInteraction,
-	query,
-}: QueryControlProps ) => {
-	const filterable = query?.filterable;
-
-	const defaultValue = useMemo( () => getDefaultValueOfFilterable(), [] );
-
-	return (
-		<ToolsPanelItem
-			label={ label }
-			hasValue={ () => filterable !== defaultValue }
-			isShownByDefault
-			onDeselect={ () => {
-				setQueryAttribute( {
-					filterable: defaultValue,
-				} );
-				trackInteraction( CoreFilterNames.FILTERABLE );
-			} }
-		>
-			<ToggleGroupControl
-				__next40pxDefaultSize
-				__nextHasNoMarginBottom
-				className="wc-block-product-collection__inherit-query-control"
-				isBlock
-				label={ label }
-				help={ filterable ? defaultFilterableHelpText : customHelpText }
-				value={ !! filterable ? 'default' : 'custom' }
-				onChange={ ( value: 'default' | 'custom' ) => {
-					setQueryAttribute( {
-						filterable: value === 'default',
-					} );
-					trackInteraction( CoreFilterNames.FILTERABLE );
-				} }
-			>
-				<ToggleGroupControlOption
-					value="default"
-					label={ defaultOptionLabel }
-				/>
-				<ToggleGroupControlOption
-					value="custom"
-					label={ customOptionLabel }
-				/>
-			</ToggleGroupControl>
-		</ToolsPanelItem>
-	);
-};
-
-export { FilterableControl, InheritQueryControl };
+export { InheritQueryControl };
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx
index 5f8788102b3..4e7fc94bc3c 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/edit/product-collection-content.tsx
@@ -24,11 +24,7 @@ import {
 	WidthOptions,
 } from '../types';
 import { DEFAULT_ATTRIBUTES, INNER_BLOCKS_TEMPLATE } from '../constants';
-import {
-	getDefaultValueOfInherit,
-	getDefaultValueOfFilterable,
-	useSetPreviewState,
-} from '../utils';
+import { getDefaultValueOfInherit, useSetPreviewState } from '../utils';
 import InspectorControls from './inspector-controls';
 import InspectorAdvancedControls from './inspector-advanced-controls';
 import ToolbarControls from './toolbar-controls';
@@ -116,7 +112,6 @@ const ProductCollectionContent = ( {
 		query: {
 			...( DEFAULT_ATTRIBUTES.query as ProductCollectionQuery ),
 			inherit: getDefaultValueOfInherit(),
-			filterable: getDefaultValueOfFilterable(),
 		},
 		...( attributes as Partial< ProductCollectionAttributes > ),
 		queryId,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/editor-contracts.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/editor-contracts.tsx
index 345553c2667..10d7b26d9f1 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/editor-contracts.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/test/editor-contracts.tsx
@@ -36,7 +36,6 @@ import {
 } from '../constants';
 import {
 	addProductCollectionToQueryPaginationParentOrAncestor,
-	getDefaultValueOfFilterable,
 	getDefaultValueOfInherit,
 } from '../utils';
 import {
@@ -250,30 +249,6 @@ describe( 'Product Collection page-context defaults', () => {
 			property: 'inherit',
 			templateSlug: 'taxonomy-product_brand',
 		},
-		{
-			caseName: 'post filtering',
-			getDefault: getDefaultValueOfFilterable,
-			property: 'filterable',
-			templateSlug: 'post',
-		},
-		{
-			caseName: 'home filtering',
-			getDefault: getDefaultValueOfFilterable,
-			property: 'filterable',
-			templateSlug: 'home',
-		},
-		{
-			caseName: 'index filtering',
-			getDefault: getDefaultValueOfFilterable,
-			property: 'filterable',
-			templateSlug: 'index',
-		},
-		{
-			caseName: 'Single Product filtering',
-			getDefault: getDefaultValueOfFilterable,
-			property: 'filterable',
-			templateSlug: 'single-product',
-		},
 	] as const )(
 		'allows only the first collection to own $caseName',
 		( { getDefault, property, templateSlug } ) => {
@@ -316,7 +291,6 @@ describe( 'Product Collection page-context defaults', () => {
 const createPreviewAttributes = (): ProductCollectionAttributes => ( {
 	...DEFAULT_ATTRIBUTES,
 	convertedFromProducts: false,
-	filterable: false,
 	hideControls: [],
 	query: {
 		...DEFAULT_QUERY,
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/tracks-utils.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/tracks-utils.ts
index b583a7a3e8d..8c96852d5d6 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/tracks-utils.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/tracks-utils.ts
@@ -22,6 +22,7 @@ const templateSlugToTemplateMap: {
 	'archive-product': Locations.PRODUCT_CATALOG,
 	'taxonomy-product_cat': Locations.PRODUCT_ARCHIVE,
 	'taxonomy-product_tag': Locations.PRODUCT_ARCHIVE,
+	'taxonomy-product_brand': Locations.PRODUCT_ARCHIVE,
 	'taxonomy-product_attribute': Locations.PRODUCT_ARCHIVE,
 	'product-search-results': Locations.PRODUCT_ARCHIVE,
 	'order-confirmation': Locations.ORDER_CONFIRMATION,
@@ -55,7 +56,8 @@ export const useTracksLocation = ( templateSlug: string | undefined ) => {

 	if (
 		templateSlug.includes( 'taxonomy-product_cat' ) ||
-		templateSlug.includes( 'taxonomy-product_tag' )
+		templateSlug.includes( 'taxonomy-product_tag' ) ||
+		templateSlug.includes( 'taxonomy-product_brand' )
 	) {
 		return Locations.PRODUCT_ARCHIVE;
 	}
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
index d4fdb2b4317..d1cb54043f8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/types.ts
@@ -41,7 +41,6 @@ export interface ProductCollectionAttributes {
 	 */
 	queryContextIncludes: string[];
 	forcePageReload: boolean;
-	filterable: boolean;
 }

 export enum LayoutOptions {
@@ -116,7 +115,6 @@ export interface ProductCollectionQuery {
 	isProductCollectionBlock: boolean;
 	woocommerceHandPickedProducts: string[];
 	priceRange: undefined | PriceRange;
-	filterable: boolean;
 	productReference?: number;
 	/**
 	 * If `null`/`undefined`: Infer from location context.
@@ -207,7 +205,6 @@ export enum CoreFilterNames {
 	STOCK_STATUS = 'stock-status',
 	TAXONOMY = 'taxonomy',
 	PRICE_RANGE = 'price-range',
-	FILTERABLE = 'filterable',
 	PRODUCTS_PER_PAGE = 'products-per-page',
 	MAX_PAGES_TO_SHOW = 'max-pages-to-show',
 	OFFSET = 'offset',
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
index 1a440a559f7..65727800303 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-collection/utils.tsx
@@ -125,9 +125,7 @@ const isInProductArchive = () => {
 	return false;
 };

-const isFirstBlockThatUsesPageContext = (
-	property: 'inherit' | 'filterable'
-) => {
+const isFirstBlockThatUsesPageContext = () => {
 	// eslint-disable-next-line @typescript-eslint/ban-ts-comment
 	// @ts-ignore No types for this exist yet, natively.
 	const { getBlocksByName, getBlock } = select( blockEditorStore );
@@ -139,7 +137,7 @@ const isFirstBlockThatUsesPageContext = (
 		( clientId ) => {
 			const block = getBlock( clientId );

-			return block.attributes?.query?.[ property ];
+			return block.attributes?.query?.inherit;
 		}
 	);

@@ -147,15 +145,7 @@ const isFirstBlockThatUsesPageContext = (
 };

 export function getDefaultValueOfInherit() {
-	return isInProductArchive()
-		? isFirstBlockThatUsesPageContext( 'inherit' )
-		: false;
-}
-
-export function getDefaultValueOfFilterable() {
-	return ! isInProductArchive()
-		? isFirstBlockThatUsesPageContext( 'filterable' )
-		: false;
+	return isInProductArchive() ? isFirstBlockThatUsesPageContext() : false;
 }

 /**
@@ -501,7 +491,6 @@ export const getDefaultQueryForSettingsSection = (
 	orderBy: DEFAULT_QUERY.orderBy as TProductCollectionOrderBy,
 	order: DEFAULT_QUERY.order as TProductCollectionOrder,
 	inherit: getDefaultValueOfInherit(),
-	filterable: getDefaultValueOfFilterable(),
 	perPage: DEFAULT_QUERY.perPage,
 	offset: DEFAULT_QUERY.offset,
 	pages: DEFAULT_QUERY.pages,
@@ -526,7 +515,6 @@ export const getDefaultProductCollection = () =>
 			query: {
 				...DEFAULT_ATTRIBUTES.query,
 				inherit: getDefaultValueOfInherit(),
-				filterable: getDefaultValueOfFilterable(),
 			},
 		},
 		createBlocksFromInnerBlocksTemplate( INNER_BLOCKS_TEMPLATE )
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
index 40142c6b57d..45a97d33510 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/__tests__/frontend.test.ts
@@ -302,19 +302,20 @@ describe( 'product filters interactivity store', () => {
 			// producing `%25D4%25B1%25D5%25B8%25D6%2582%25D5%25A4%25D5%25AB%25D5%25B8%25D5%25A3%25D5%25AB%25D6%2580%25D6%2584`
 			// instead of the intended `%D4%B1%D5%B8%D6%82%D5%A4%D5%AB%D5%B8%D5%A3%D5%AB%D6%80%D6%84`.
 			expectedUrl:
-				'https://example.com/shop/?color=%D4%B1%D5%B8%D6%82%D5%A4%D5%AB%D5%B8%D5%A3%D5%AB%D6%80%D6%84',
+				'https://example.com/shop/?existing=1&color=%D4%B1%D5%B8%D6%82%D5%A4%D5%AB%D5%B8%D5%A3%D5%AB%D6%80%D6%84',
 		},
 		{
 			description: 'latin value',
 			label: 'Blue',
 			value: 'blue',
-			expectedUrl: 'https://example.com/shop/?color=blue',
+			expectedUrl: 'https://example.com/shop/?existing=1&color=blue',
 		},
 		{
 			description: 'malformed encoded value',
 			label: 'Invalid',
 			value: '%E0%A4%A',
-			expectedUrl: 'https://example.com/shop/?color=%25E0%25A4%25A',
+			expectedUrl:
+				'https://example.com/shop/?existing=1&color=%25E0%25A4%25A',
 			expectConsoleWarn: true,
 		},
 	].forEach(
@@ -409,6 +410,171 @@ describe( 'product filters interactivity store', () => {
 		}
 	);

+	it.each( [ 0, 17 ] )(
+		'scopes navigation and clearing to collection %i',
+		( queryId ) => {
+			if ( ! mockRegisteredStore ) {
+				throw new Error( 'Product filters store was not registered.' );
+			}
+			window.history.replaceState(
+				{},
+				'',
+				`https://example.com/shop/?min_price=5&wcp-${ queryId }-min_price=10&wcp-${ queryId }-maker=old&wcp-28-min_price=60&query-${ queryId }-page=3&query-28-page=2&campaign=spring`
+			);
+			const context = {
+				queryId,
+				params: { min_price: '10', maker: 'old' },
+				activeFilters: [
+					{
+						type: 'taxonomy/product_brand',
+						value: 'acme',
+						activeLabel: 'Acme',
+					},
+				],
+				forcePageReload: true,
+			};
+			mockGetContext.mockReturnValue( context );
+			mockGetServerContext.mockReturnValue( context );
+			mockGetConfig.mockReturnValue( {
+				canonicalUrl: 'https://example.com/shop/',
+				filterParamKeys: [ 'min_price', 'maker' ],
+				taxonomyParamsMap: { product_brand: 'maker' },
+			} );
+
+			expect( mockRegisteredStore.state.params ).toEqual( {
+				maker: 'acme',
+			} );
+			mockRegisteredStore.actions.navigate().next();
+			expect( mockReload ).toHaveBeenLastCalledWith(
+				`https://example.com/shop/?min_price=5&wcp-28-min_price=60&query-28-page=2&campaign=spring&wcp-${ queryId }-maker=acme`
+			);
+
+			context.activeFilters = [];
+			mockRegisteredStore.actions.navigate().next();
+			expect( mockReload ).toHaveBeenLastCalledWith(
+				'https://example.com/shop/?min_price=5&wcp-28-min_price=60&query-28-page=2&campaign=spring'
+			);
+		}
+	);
+
+	it.each( [ 'completion', 'failure', 'history', 'other navigation' ] )(
+		'preserves overlapping changes and resets pending state on %s',
+		( finish ) => {
+			if ( ! mockRegisteredStore ) {
+				throw new Error( 'Product filters store was not registered.' );
+			}
+			const { actions } = mockRegisteredStore;
+			window.history.replaceState(
+				{},
+				'',
+				'https://example.com/shop/?query-0-page=2&query-28-page=3'
+			);
+			mockGetConfig.mockReturnValue( {
+				filterParamKeys: [ 'categories' ],
+				taxonomyParamsMap: { product_cat: 'categories' },
+			} );
+			const routerNavigate = jest.fn();
+			const router = {
+				actions: { navigate: routerNavigate },
+			} as unknown as typeof import( '@wordpress/interactivity-router' );
+			const navigate = ( queryId: number, value: string ) => {
+				const context = {
+					queryId,
+					params: {},
+					activeFilters: value
+						? [ { type: 'taxonomy/product_cat', value } ]
+						: [],
+				};
+				mockGetContext.mockReturnValue( context );
+				mockGetServerContext.mockReturnValue( context );
+				const iterator = actions.navigate();
+				iterator.next();
+				iterator.next( router );
+				return iterator;
+			};
+
+			const first = navigate( 0, 'shirts' );
+			const second = navigate( 28, 'hats' );
+			expect( routerNavigate ).toHaveBeenLastCalledWith(
+				'https://example.com/shop/?wcp-0-categories=shirts&wcp-28-categories=hats'
+			);
+			first.next();
+			const clear = navigate( 0, '' );
+			expect( routerNavigate ).toHaveBeenLastCalledWith(
+				'https://example.com/shop/?wcp-28-categories=hats'
+			);
+			second.next();
+			let error;
+			try {
+				if ( finish === 'failure' ) {
+					clear.throw( new Error( 'Navigation failed' ) );
+				}
+			} catch ( caught ) {
+				error = caught;
+			}
+			expect( error ).toEqual(
+				finish === 'failure'
+					? new Error( 'Navigation failed' )
+					: undefined
+			);
+			if ( finish === 'history' ) {
+				window.dispatchEvent( new PopStateEvent( 'popstate' ) );
+			} else if ( finish === 'other navigation' ) {
+				window.history.replaceState( {}, '', '/shop/?campaign=summer' );
+			} else {
+				clear.next();
+			}
+			const next = navigate( 0, 'boots' );
+			expect( routerNavigate ).toHaveBeenLastCalledWith(
+				finish === 'other navigation'
+					? 'https://example.com/shop/?campaign=summer&wcp-0-categories=boots'
+					: 'https://example.com/shop/?query-28-page=3&wcp-0-categories=boots'
+			);
+			next.next();
+		}
+	);
+
+	it( 'does not restore cleared local parameters from the global canonical URL', () => {
+		if ( ! mockRegisteredStore ) {
+			throw new Error( 'Product filters store was not registered.' );
+		}
+		window.history.replaceState( {}, '', '/shop/?wcp-0-categories=shirts' );
+		mockGetConfig.mockReturnValue( {
+			canonicalUrl: 'https://example.com/shop/?wcp-0-categories=shirts',
+			filterParamKeys: [ 'categories' ],
+			taxonomyParamsMap: { product_cat: 'categories' },
+		} );
+		mockGetContext.mockReturnValue( { activeFilters: [] } );
+		mockGetServerContext.mockReturnValue( { queryId: 0, params: {} } );
+		const local = mockRegisteredStore.actions.navigate();
+		local.next();
+		mockGetServerContext.mockReturnValue( { params: {} } );
+		mockGetContext.mockReturnValue( {
+			activeFilters: [ { type: 'taxonomy/product_cat', value: 'hats' } ],
+		} );
+		const global = mockRegisteredStore.actions.navigate();
+		global.next();
+		const navigate = jest.fn();
+		const router = {
+			actions: { navigate },
+		} as unknown as typeof import( '@wordpress/interactivity-router' );
+		global.next( router );
+		local.next( router );
+		expect( navigate.mock.calls ).toEqual( [
+			[ 'https://example.com/shop/?categories=hats' ],
+		] );
+		window.history.replaceState( {}, '', '/shop/?categories=hats' );
+		global.next();
+		mockGetContext.mockReturnValue( { activeFilters: [] } );
+		const clear = mockRegisteredStore.actions.navigate();
+		clear.next();
+		clear.next( router );
+		expect( navigate ).toHaveBeenLastCalledWith(
+			'https://example.com/shop/'
+		);
+		clear.next();
+	} );
+
 	it( 'triggers a full-page reload instead of router when forcePageReload is true', () => {
 		if ( ! mockRegisteredStore ) {
 			throw new Error( 'Product filters store was not registered.' );
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
index c1aacb6e77d..59c8bd1f30e 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/block.json
@@ -37,7 +37,14 @@
 		}
 	},
 	"textdomain": "woocommerce",
-	"usesContext": [ "postId", "query", "queryId", "forcePageReload" ],
+	"usesContext": [
+		"postId",
+		"query",
+		"queryId",
+		"collection",
+		"productCollectionLocation",
+		"forcePageReload"
+	],
 	"attributes": {
 		"isPreview": {
 			"type": "boolean",
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
index 2b066fe306d..9d937799965 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/frontend.ts
@@ -30,6 +30,12 @@ const {

 const BLOCK_NAME = PRODUCT_FILTERS_STORE_NAME;

+let pendingNavigation: { source: string; destination: string } | undefined;
+
+window.addEventListener( 'popstate', () => {
+	pendingNavigation = undefined;
+} );
+
 type ValidFilterOptionItem = FilterOptionItem & {
 	type: string;
 	value: string;
@@ -279,11 +285,28 @@ const productFiltersStore = {
 				: getContext< ProductFiltersContext >();

 			const config = getConfig( BLOCK_NAME );
-			const url = new URL( config.canonicalUrl );
+			const { queryId } = context;
+			const prefix = queryId === undefined ? '' : `wcp-${ queryId }-`;
+			const currentUrl =
+				pendingNavigation?.source === window.location.href
+					? pendingNavigation.destination
+					: window.location.href;
+			const url = new URL(
+				queryId === undefined ? config.canonicalUrl : currentUrl
+			);
+			url.search = new URL( currentUrl ).search;
 			const { searchParams } = url;

-			for ( const key in context.params ) {
-				searchParams.delete( key );
+			if ( queryId === undefined ) {
+				searchParams.delete( 'paged' );
+				searchParams.delete( 'product-page' );
+			} else {
+				searchParams.delete( `query-${ queryId }-page` );
+			}
+
+			for ( const key of config.filterParamKeys ??
+				Object.keys( context.params ) ) {
+				searchParams.delete( `${ prefix }${ key }` );
 			}

 			for ( const key in state.params ) {
@@ -303,10 +326,10 @@ const productFiltersStore = {
 					}
 				}

-				searchParams.set( key, decodedValue );
+				searchParams.set( `${ prefix }${ key }`, decodedValue );
 			}

-			if ( window.location.href === url.href ) {
+			if ( currentUrl === url.href ) {
 				return;
 			}

@@ -323,10 +346,23 @@ const productFiltersStore = {
 				return;
 			}

-			const routerModule: typeof import( '@wordpress/interactivity-router' ) =
-				yield import( '@wordpress/interactivity-router' );
-
-			yield routerModule.actions.navigate( url.href );
+			const navigation = {
+				source: window.location.href,
+				destination: url.href,
+			};
+			pendingNavigation = navigation;
+			try {
+				const routerModule: typeof import( '@wordpress/interactivity-router' ) =
+					yield import( '@wordpress/interactivity-router' );
+
+				if ( pendingNavigation === navigation ) {
+					yield routerModule.actions.navigate( url.href );
+				}
+			} finally {
+				if ( pendingNavigation === navigation ) {
+					pendingNavigation = undefined;
+				}
+			}
 		},
 	},
 	callbacks: {
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/block.json
index f47cfdf2176..3dc9e97ea72 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/attribute-filter/block.json
@@ -53,7 +53,14 @@
 			}
 		}
 	},
-	"usesContext": [ "query", "filterParams", "productFiltersKey" ],
+	"usesContext": [
+		"query",
+		"queryId",
+		"collection",
+		"productCollectionLocation",
+		"filterParams",
+		"productFiltersKey"
+	],
 	"attributes": {
 		"attributeId": {
 			"type": "number",
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/block.json
index 1852f94d674..653373cfdfa 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/price-filter/block.json
@@ -12,6 +12,13 @@
 		"interactivity": true,
 		"html": false
 	},
-	"usesContext": [ "query", "filterParams", "productFiltersKey" ],
+	"usesContext": [
+		"query",
+		"queryId",
+		"collection",
+		"productCollectionLocation",
+		"filterParams",
+		"productFiltersKey"
+	],
 	"viewScriptModule": "woocommerce/product-filter-price"
 }
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/block.json
index 303c990803d..78eb10db2c8 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/rating-filter/block.json
@@ -12,7 +12,14 @@
 		}
 	},
 	"ancestor": [ "woocommerce/product-filters" ],
-	"usesContext": [ "query", "filterParams", "productFiltersKey" ],
+	"usesContext": [
+		"query",
+		"queryId",
+		"collection",
+		"productCollectionLocation",
+		"filterParams",
+		"productFiltersKey"
+	],
 	"attributes": {
 		"className": {
 			"type": "string",
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/status-filter/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/status-filter/block.json
index 975c7ec1daa..da4c3a40bed 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/status-filter/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/status-filter/block.json
@@ -71,7 +71,14 @@
 			"default": true
 		}
 	},
-	"usesContext": [ "query", "filterParams", "productFiltersKey" ],
+	"usesContext": [
+		"query",
+		"queryId",
+		"collection",
+		"productCollectionLocation",
+		"filterParams",
+		"productFiltersKey"
+	],
 	"example": {
 		"attributes": {
 			"isPreview": true
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/taxonomy-filter/block.json b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/taxonomy-filter/block.json
index c9fb404d605..4f983aedeb4 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/taxonomy-filter/block.json
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/inner-blocks/taxonomy-filter/block.json
@@ -53,7 +53,14 @@
 			}
 		}
 	},
-	"usesContext": [ "query", "filterParams", "productFiltersKey" ],
+	"usesContext": [
+		"query",
+		"queryId",
+		"collection",
+		"productCollectionLocation",
+		"filterParams",
+		"productFiltersKey"
+	],
 	"attributes": {
 		"taxonomy": {
 			"type": "string",
diff --git a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/types.ts b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/types.ts
index 76f8863a4bb..bcf9127ce7d 100644
--- a/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/types.ts
+++ b/plugins/woocommerce/client/blocks/assets/js/blocks/product-filters/types.ts
@@ -36,6 +36,7 @@ export type ActiveFilterItem = {

 export type ProductFiltersContext = {
 	isOverlayOpened: boolean;
+	queryId?: number;
 	openButtonRef?: HTMLElement | null;
 	params: Record< string, string >;
 	activeFilters: ActiveFilterItem[];
diff --git a/plugins/woocommerce/client/blocks/packages/public-api/blocks-registry/product-collection/test/register-product-collection.tsx b/plugins/woocommerce/client/blocks/packages/public-api/blocks-registry/product-collection/test/register-product-collection.tsx
index de780b009f4..12d3d976c24 100644
--- a/plugins/woocommerce/client/blocks/packages/public-api/blocks-registry/product-collection/test/register-product-collection.tsx
+++ b/plugins/woocommerce/client/blocks/packages/public-api/blocks-registry/product-collection/test/register-product-collection.tsx
@@ -278,7 +278,6 @@ describe( '__experimentalRegisterProductCollection', () => {
 				query: {
 					exclude: [],
 					featured: false,
-					filterable: false,
 					inherit: false,
 					isProductCollectionBlock: true,
 					offset: 0,
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Controller.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Controller.php
index 9f965dbc9b5..b1d7a9e557d 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Controller.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Controller.php
@@ -392,10 +392,9 @@ class Controller extends AbstractBlock {
 		$block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array();
 		$block_context_query['queryId']   = $block->context['queryId'] ?? null;

-		$inherit    = $block->context['query']['inherit'] ?? false;
-		$filterable = $block->context['query']['filterable'] ?? false;
-
-		$is_exclude_applied_filters = ! ( $inherit || $filterable );
+		if ( ! ( $block_context_query['inherit'] ?? false ) ) {
+			$block_context_query['filterParams'] = wc_get_container()->get( \Automattic\WooCommerce\Internal\ProductFilters\Params::class )->get_filter_params( $block_context_query['queryId'] );
+		}

 		$collection_args = array(
 			'name'                      => $block->context['collection'] ?? '',
@@ -406,8 +405,7 @@ class Controller extends AbstractBlock {
 		return $this->query_builder->get_final_frontend_query(
 			$collection_args,
 			$block_context_query,
-			$page,
-			$is_exclude_applied_filters
+			$page
 		);
 	}

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index a0b45460736..8672a4d0f1f 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -204,6 +204,7 @@ class QueryBuilder {
 				'timeFrame'           => $time_frame,
 				'priceRange'          => $price_range,
 				'queryId'             => $query_id,
+				'filterParams'        => $query['filterParams'] ?? null,
 			),
 			$is_exclude_applied_filters
 		);
@@ -313,8 +314,9 @@ class QueryBuilder {

 		if ( ! $is_exclude_applied_filters ) {
 			// Rating must be a tax query; the remaining filters are query vars consumed by QueryClauses.
-			$final_query = $this->merge_queries( $final_query, $this->get_filter_by_rating_query() );
-			$final_query = array_merge( $final_query, $this->get_applied_filter_query_vars() );
+			$filter_params = $query['filterParams'] ?? null;
+			$final_query   = $this->merge_queries( $final_query, $this->get_filter_by_rating_query( $filter_params ) );
+			$final_query   = array_merge( $final_query, $this->get_applied_filter_query_vars( $filter_params ) );
 		}

 		return $final_query;
@@ -512,10 +514,11 @@ class QueryBuilder {
 	/**
 	 * Return a query that filters products by rating.
 	 *
+	 * @param array|null $filter_params Local selections, or null for global query vars.
 	 * @return array
 	 */
-	private function get_filter_by_rating_query() {
-		$filter_rating_values = get_query_var( RatingFilter::RATING_QUERY_VAR );
+	private function get_filter_by_rating_query( ?array $filter_params = null ) {
+		$filter_rating_values = null === $filter_params ? get_query_var( RatingFilter::RATING_QUERY_VAR ) : ( $filter_params[ RatingFilter::RATING_QUERY_VAR ] ?? '' );
 		if ( empty( $filter_rating_values ) ) {
 			return array();
 		}
@@ -616,9 +619,10 @@ class QueryBuilder {
 	/**
 	 * Get the active Product Filter URL query variables.
 	 *
+	 * @param array|null $filter_params Local selections, or null for global query vars.
 	 * @return array
 	 */
-	private function get_applied_filter_query_vars(): array {
+	private function get_applied_filter_query_vars( ?array $filter_params = null ): array {
 		$params                          = wc_get_container()->get( Params::class );
 		$filter_query_vars               = array();
 		$registered_attribute_params     = $params->get_param( 'attribute' );
@@ -635,14 +639,14 @@ class QueryBuilder {
 				continue;
 			}

-			$value = get_query_var( $param_key );
+			$value = null === $filter_params ? get_query_var( $param_key ) : ( $filter_params[ $param_key ] ?? '' );
 			if ( ( is_string( $value ) || is_numeric( $value ) ) && '' !== (string) $value ) {
 				$filter_query_vars[ $param_key ] = (string) $value;
 			}
 		}

 		foreach ( $this->get_filter_by_attributes_query_vars() as $attribute_name => $query_args ) {
-			$filter_value = get_query_var( $query_args['filter'] );
+			$filter_value = null === $filter_params ? get_query_var( $query_args['filter'] ) : ( $filter_params[ $query_args['filter'] ] ?? '' );
 			if ( ( ! is_string( $filter_value ) && ! is_numeric( $filter_value ) ) || '' === (string) $filter_value ) {
 				continue;
 			}
@@ -651,7 +655,7 @@ class QueryBuilder {
 				$attribute_name = preg_replace( '/^(filter_|pa_)/', '', $query_args['filter'] );
 			}
 			$attribute_name = wc_sanitize_taxonomy_name( $attribute_name );
-			$query_type     = get_query_var( $query_args['query_type'] );
+			$query_type     = null === $filter_params ? get_query_var( $query_args['query_type'] ) : ( $filter_params[ $query_args['query_type'] ] ?? '' );

 			$filter_query_vars[ AttributeFilter::FILTER_QUERY_VAR_PREFIX . $attribute_name ]     = (string) $filter_value;
 			$filter_query_vars[ AttributeFilter::QUERY_TYPE_QUERY_VAR_PREFIX . $attribute_name ] = is_string( $query_type ) && in_array( $query_type, array( 'and', 'or' ), true )
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
index 7973e7913ff..195350e4b0f 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Renderer.php
@@ -86,6 +86,17 @@ class Renderer {
 			100,
 			1
 		);
+		add_filter(
+			'render_block_woocommerce/product-filters',
+			function ( $html, $parsed_block, $instance ) {
+				if ( $instance->context['query']['isProductCollectionBlock'] ?? false ) {
+					$this->render_state['has_filters'] = ! empty( $html );
+				}
+				return $html;
+			},
+			10,
+			3
+		);
 		add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
 		add_filter( 'render_block_context', array( $this, 'handle_render_block_context' ), 11, 3 );
 	}
@@ -179,7 +190,7 @@ class Renderer {
 	 * @return bool
 	 */
 	private function should_prevent_render() {
-		return ! $this->render_state['has_results'] && ! $this->render_state['has_no_results_block'];
+		return ! $this->render_state['has_results'] && ! $this->render_state['has_no_results_block'] && empty( $this->render_state['has_filters'] );
 	}

 	/**
@@ -476,6 +487,11 @@ class Renderer {
 			return $context;
 		}

+		if ( 'woocommerce/product-filters' === $block_name && ( $context['query']['inherit'] ?? false ) ) {
+			// A null override also clears the saved ID when WordPress refreshes available context.
+			$context['queryId'] = null;
+		}
+
 		// Check parent block contexts (in order of specificity).
 		$is_in_single_product = isset( $context['singleProduct'] ) && ! empty( $context['postId'] );

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
index 068c018c6d5..cadce01c840 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterAttribute.php
@@ -286,7 +286,8 @@ final class ProductFilterAttribute extends AbstractBlock {
 	 * @param string   $query_type Query type, accept 'and' or 'or'.
 	 */
 	private function get_attribute_counts( $block, $slug, $query_type ) {
-		if ( ! isset( $block->context['filterParams'] ) ) {
+		$context = $block->context;
+		if ( ! isset( $context['filterParams'] ) ) {
 			return array();
 		}

@@ -303,7 +304,7 @@ final class ProductFilterAttribute extends AbstractBlock {
 			);
 		}

-		if ( ! empty( $query_vars['tax_query'] ) ) {
+		if ( ! isset( $context['queryId'] ) && ! empty( $query_vars['tax_query'] ) ) {
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
 			$query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], 'taxonomy', $slug );
 		}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
index df9e6d4dcde..0091e413de4 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilterTaxonomy.php
@@ -376,7 +376,8 @@ final class ProductFilterTaxonomy extends AbstractBlock {
 	 * @return array Term counts with term_id as key and count as value.
 	 */
 	private function get_taxonomy_term_counts( $block, $taxonomy ) {
-		if ( ! isset( $block->context['filterParams'] ) ) {
+		$context = $block->context;
+		if ( ! isset( $context['filterParams'] ) ) {
 			return array();
 		}

@@ -406,7 +407,7 @@ final class ProductFilterTaxonomy extends AbstractBlock {
 		}

 		// Remove from tax_query if present.
-		if ( ! empty( $query_vars['tax_query'] ) ) {
+		if ( ! isset( $context['queryId'] ) && ! empty( $query_vars['tax_query'] ) ) {
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
 			$query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], 'taxonomy', $taxonomy );
 		}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
index cdaf3e120b0..8bd250501da 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductFilters.php
@@ -28,7 +28,7 @@ class ProductFilters extends AbstractBlock {
 	 * @return string[]
 	 */
 	protected function get_block_type_uses_context() {
-		return array( 'postId', 'query', 'queryId', 'forcePageReload' );
+		return array( 'postId', 'query', 'queryId', 'collection', 'productCollectionLocation', 'forcePageReload' );
 	}

 	/**
@@ -176,10 +176,17 @@ CSS;

 		wp_enqueue_script( 'wc-settings' );

-		$query_id      = $block->context['queryId'] ?? 0;
-		$filter_params = $this->get_filter_params( $query_id );
+		$query_id      = $block->context['queryId'] ?? null;
+		$params        = wc_get_container()->get( Params::class );
+		$filter_params = $params->get_filter_params( $query_id );

-		wp_interactivity_config( $this->get_full_block_name(), [ 'canonicalUrl' => $this->get_canonical_url_no_pagination( $filter_params ) ] );
+		wp_interactivity_config(
+			$this->get_full_block_name(),
+			array(
+				'canonicalUrl'    => $this->get_canonical_url_no_pagination( $params->get_filter_params() ),
+				'filterParamKeys' => $params->get_param_keys(),
+			)
+		);

 		/**
 		 * Filter hook to modify the selected filter items.
@@ -220,6 +227,9 @@ CSS;
 			// frontend can fall back to the global interactivity config.
 			'forcePageReload' => isset( $block->context['forcePageReload'] ) ? (bool) $block->context['forcePageReload'] : null,
 		);
+		if ( null !== $query_id ) {
+			$interactivity_context['queryId'] = $query_id;
+		}

 		$overlay_mode     = $this->get_overlay_mode( $attributes );
 		$has_overlay      = 'off' !== $overlay_mode;
@@ -362,43 +372,13 @@ CSS;
 					array(
 						'attrs'       => $block->parsed_block['attrs'] ?? array(),
 						'innerBlocks' => $block->parsed_block['innerBlocks'] ?? array(),
+						'queryId'     => $block->context['queryId'] ?? null,
 					)
 				)
 			)
 		);
 	}

-	/**
-	 * Parse the filter parameters from the URL.
-	 * For now we only get the global query params from the URL. In the future,
-	 * we should get the query params based on $query_id.
-	 *
-	 * @param int $query_id Query ID.
-	 * @return array Parsed filter params.
-	 */
-	private function get_filter_params( $query_id ) {
-		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
-
-		$parsed_url = wp_parse_url( esc_url_raw( $request_uri ) );
-
-		if ( empty( $parsed_url['query'] ) ) {
-			return array();
-		}
-
-		parse_str( $parsed_url['query'], $url_query_params );
-
-		$filter_param_keys = wc_get_container()->get( Params::class )->get_param_keys();
-
-		return array_filter(
-			$url_query_params,
-			function ( $key ) use ( $filter_param_keys ) {
-				return in_array( $key, $filter_param_keys, true );
-			},
-			ARRAY_FILTER_USE_KEY
-		);
-	}
-
 	/**
 	 * Disable the style handle for this block type. We use block.json to load the style.
 	 *
diff --git a/plugins/woocommerce/src/Internal/ProductFilters/Params.php b/plugins/woocommerce/src/Internal/ProductFilters/Params.php
index 10b42c30263..f52fcf6f493 100644
--- a/plugins/woocommerce/src/Internal/ProductFilters/Params.php
+++ b/plugins/woocommerce/src/Internal/ProductFilters/Params.php
@@ -48,6 +48,43 @@ class Params implements FilterUrlParam {
 		return $keys;
 	}

+	/**
+	 * Read one collection's filter selections from the request URL.
+	 *
+	 * @since 11.4.0
+	 *
+	 * @param int|null $query_id Collection ID, or null for global filters.
+	 * @return array<string, string> Selections keyed by effective filter name.
+	 */
+	public function get_filter_params( ?int $query_id = null ): array {
+		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are validated below against registered filter parameters.
+		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
+		$query       = wp_parse_url( $request_uri, PHP_URL_QUERY );
+		$url_params  = array();
+		parse_str( is_string( $query ) ? $query : '', $url_params );
+		$prefix = null === $query_id ? '' : 'wcp-' . $query_id . '-';
+		$params = array();
+
+		foreach ( $this->get_param_keys() as $key ) {
+			$value = $url_params[ $prefix . $key ] ?? null;
+			if ( ! is_string( $value ) || '' === $value ) {
+				continue;
+			}
+			if ( in_array( $key, $this->get_param( 'price' ), true ) && ( ! is_numeric( $value ) || ! is_finite( (float) $value ) || (float) $value < 0 ) ) {
+				continue;
+			}
+			if ( 'rating_filter' === $key && ! preg_match( '/^[1-5](,[1-5])*$/', $value ) ) {
+				continue;
+			}
+			if ( 0 === strpos( $key, 'query_type_' ) && ! in_array( $value, array( 'and', 'or' ), true ) ) {
+				continue;
+			}
+			$params[ $key ] = $value;
+		}
+
+		return $params;
+	}
+
 	/**
 	 * Get the param.
 	 *
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collections.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collections.block_theme.spec.ts
index d69486af313..3b834b75731 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collections.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/collections.block_theme.spec.ts
@@ -118,7 +118,7 @@ test.describe( 'Product Collection: Collections', () => {
 		await expect( pageObject.products ).toHaveCount( 4 );
 	} );

-	test( 'Product Catalog collection can be added in post and syncs query with template', async ( {
+	test( 'Product Catalog collection can be added in post with a local query', async ( {
 		pageObject,
 	} ) => {
 		await pageObject.createNewPostAndInsertBlock( 'productCatalog' );
@@ -127,10 +127,7 @@ test.describe( 'Product Collection: Collections', () => {
 			.locateSidebarSettings()
 			.getByLabel( SELECTORS.usePageContextControl );

-		await expect( queryTypeLocator.getByLabel( 'Default' ) ).toBeChecked();
-		await expect(
-			queryTypeLocator.getByLabel( 'Custom' )
-		).not.toBeChecked();
+		await expect( queryTypeLocator ).toBeHidden();
 		await expect( pageObject.products ).toHaveCount( 9 );

 		await pageObject.publishAndGoToFrontend();
diff --git a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/inspector-controls.block_theme.spec.ts b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/inspector-controls.block_theme.spec.ts
index 95b3b07e3ce..78d6dc94da9 100644
--- a/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/inspector-controls.block_theme.spec.ts
+++ b/plugins/woocommerce/tests/e2e/tests/blocks/product-collection/inspector-controls.block_theme.spec.ts
@@ -420,10 +420,15 @@ test.describe( 'Product Collection: Inspector Controls', () => {

 		await expect( pageObject.products ).toHaveCount( 9 );

-		await editor.insertBlock( {
-			name: 'woocommerce/filter-wrapper',
-			attributes: { filterType: 'price-filter' },
-		} );
+		const productCollectionBlock = await editor.getBlockByName(
+			'woocommerce/product-collection'
+		);
+		const productCollectionClientId =
+			( await productCollectionBlock.getAttribute( 'data-block' ) ) ?? '';
+		await editor.insertBlock(
+			{ name: 'woocommerce/product-filters' },
+			{ clientId: productCollectionClientId }
+		);

 		await pageObject.publishAndGoToFrontend();

@@ -431,14 +436,14 @@ test.describe( 'Product Collection: Inspector Controls', () => {

 		await page
 			.getByRole( 'textbox', {
-				name: 'Filter products by minimum',
+				name: 'Filter products by minimum price',
 			} )
 			.dblclick();
 		await page.keyboard.type( '45' );

 		await page
 			.getByRole( 'textbox', {
-				name: 'Filter products by maximum',
+				name: 'Filter products by maximum price',
 			} )
 			.dblclick();
 		await page.keyboard.type( '55' );
@@ -457,14 +462,14 @@ test.describe( 'Product Collection: Inspector Controls', () => {
 	} );

 	test.describe( '"Query Type" control', () => {
-		test( 'should be visible on posts', async ( { pageObject } ) => {
+		test( 'should be hidden on posts', async ( { pageObject } ) => {
 			await pageObject.createNewPostAndInsertBlock();

 			await expect(
 				pageObject
 					.locateSidebarSettings()
 					.getByLabel( SELECTORS.usePageContextControl )
-			).toBeVisible();
+			).toBeHidden();
 		} );

 		[
@@ -517,7 +522,7 @@ test.describe( 'Product Collection: Inspector Controls', () => {
 			`${ BLOCK_THEME_SLUG }//home`,
 			`${ BLOCK_THEME_SLUG }//index`,
 		].forEach( ( slug ) => {
-			test( `should be visible in non-archive template: ${ slug }`, async ( {
+			test( `should be hidden in non-archive template: ${ slug }`, async ( {
 				pageObject,
 				editor,
 			} ) => {
@@ -531,7 +536,7 @@ test.describe( 'Product Collection: Inspector Controls', () => {
 					pageObject
 						.locateSidebarSettings()
 						.getByLabel( SELECTORS.usePageContextControl )
-				).toBeVisible();
+				).toBeHidden();
 			} );
 		} );

@@ -635,7 +640,7 @@ test.describe( 'Product Collection: Inspector Controls', () => {
 			await expect( customQueryType ).not.toBeChecked();
 		} );

-		test( 'allows filtering in non-archive context', async ( {
+		test( 'nested filters affect only their local collection', async ( {
 			pageObject,
 			editor,
 			page,
@@ -685,10 +690,10 @@ test.describe( 'Product Collection: Inspector Controls', () => {

 			await expect(
 				productCollection.first().locator( SELECTORS.product )
-			).toHaveCount( 1 );
+			).toHaveCount( 9 );
 			await expect(
 				productCollection.last().locator( SELECTORS.product )
-			).toHaveCount( 9 );
+			).toHaveCount( 1 );
 		} );

 		test( 'correctly combines editor and front-end filters', async ( {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 8efcce53fd4..d8afcb2808c 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -27,6 +27,51 @@ class QueryBuilder extends \WP_UnitTestCase {
 	 */
 	private $block_instance;

+	/**
+	 * @testdox Should filter only the requested collection within its saved product selection.
+	 * @testWith [0, false]
+	 *           [17, true]
+	 * @param int  $query_id Collection ID.
+	 * @param bool $legacy_filterable Previously saved filterable value.
+	 */
+	public function test_local_filter_products( int $query_id, bool $legacy_filterable ): void {
+		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Restore the original request verbatim after the test.
+		$original_uri = $_SERVER['REQUEST_URI'] ?? null;
+		$products     = array();
+		foreach ( array( '10.25', '15.28', '5' ) as $price ) {
+			$product = WC_Helper_Product::create_simple_product();
+			$product->set_regular_price( $price );
+			$product->save();
+			$products[] = $product->get_id();
+		}
+		try {
+			$_SERVER['REQUEST_URI'] = '/?min_price=999&wcp-' . $query_id . '-max_price=10.28&wcp-28-min_price=15';
+			set_query_var( 'min_price', '999' );
+			$block                                 = Utils::get_base_parsed_block();
+			$block['attrs']['queryId']             = $query_id;
+			$block['attrs']['query']['inherit']    = false;
+			$block['attrs']['query']['filterable'] = $legacy_filterable;
+			$block['attrs']['query']['woocommerceHandPickedProducts'] = array_slice( $products, 0, 2 );
+			$query = Utils::initialize_merged_query( $this->block_instance, $block );
+			$this->assertSame( array( $products[0] ), wp_list_pluck( ( new WP_Query( $query ) )->posts, 'ID' ) );
+
+			$block['attrs']['queryId'] = 28;
+			$query                     = Utils::initialize_merged_query( $this->block_instance, $block );
+			$this->assertSame( array( $products[1] ), wp_list_pluck( ( new WP_Query( $query ) )->posts, 'ID' ) );
+
+			$_SERVER['REQUEST_URI'] = '/?min_price=999';
+			$query                  = Utils::initialize_merged_query( $this->block_instance, $block );
+			$this->assertEqualsCanonicalizing( array_slice( $products, 0, 2 ), wp_list_pluck( ( new WP_Query( $query ) )->posts, 'ID' ) );
+		} finally {
+			set_query_var( 'min_price', '' );
+			if ( null === $original_uri ) {
+				unset( $_SERVER['REQUEST_URI'] );
+			} else {
+				$_SERVER['REQUEST_URI'] = $original_uri;
+			}
+		}
+	}
+
 	/**
 	 * Initiate the mock object.
 	 */
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilters.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilters.php
index 80973557b17..28c9fbf7cfd 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilters.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductFilters.php
@@ -48,6 +48,71 @@ class ProductFilters extends \WP_UnitTestCase {
 	 */
 	private $render_method;

+	/**
+	 * @testdox Local filter counts preserve saved taxonomy restrictions, including empty collections.
+	 */
+	public function test_nested_local_filters_preserve_saved_taxonomy_and_empty_collection(): void {
+		$red  = self::factory()->term->create(
+			array(
+				'taxonomy' => 'product_cat',
+				'name'     => 'Local red',
+				'slug'     => 'local-red',
+			)
+		);
+		$blue = self::factory()->term->create(
+			array(
+				'taxonomy' => 'product_cat',
+				'name'     => 'Local blue',
+				'slug'     => 'local-blue',
+			)
+		);
+		foreach ( array( 10, 30, 40 ) as $price ) {
+			$product = \WC_Helper_Product::create_simple_product();
+			$product->set_regular_price( (string) $price );
+			$product->set_category_ids( array( 40 === $price ? $blue : $red ) );
+			$product->save();
+		}
+		$attributes = array(
+			'queryId' => 0,
+			'query'   => array(
+				'isProductCollectionBlock' => true,
+				'postType'                 => 'product',
+				'inherit'                  => false,
+				'perPage'                  => 10,
+				'taxQuery'                 => array( 'product_cat' => array( $red ) ),
+			),
+		);
+		$markup     = '<!-- wp:woocommerce/product-collection ' . wp_json_encode( $attributes ) . ' --><div class="wp-block-woocommerce-product-collection">'
+			. '<!-- wp:woocommerce/product-filters --><div class="wp-block-woocommerce-product-filters">'
+			. '<!-- wp:woocommerce/product-filter-taxonomy {"taxonomy":"product_cat","showCounts":true} --><div>'
+			. '<!-- wp:woocommerce/product-filter-checkbox-list --><div class="wc-block-product-filter-checkbox-list"></div><!-- /wp:woocommerce/product-filter-checkbox-list -->'
+			. '</div><!-- /wp:woocommerce/product-filter-taxonomy --></div><!-- /wp:woocommerce/product-filters -->'
+			. '<!-- wp:woocommerce/product-template --><!-- wp:post-title /--><!-- /wp:woocommerce/product-template -->'
+			. '</div><!-- /wp:woocommerce/product-collection -->';
+		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Restore the original request verbatim after the test.
+		$original_uri = $_SERVER['REQUEST_URI'] ?? null;
+		try {
+			$_SERVER['REQUEST_URI'] = '/?wcp-0-categories=local-blue&wcp-0-min_price=20&min_price=999';
+			$html                   = do_blocks( $markup );
+			$this->assertStringContainsString( 'wp-block-woocommerce-product-collection', $html );
+			$this->assertStringNotContainsString( 'wp-block-post-title', $html );
+			$this->assertStringContainsString( 'aria-label="Local red"', $html );
+			$this->assertStringNotContainsString( 'aria-label="Local blue"', $html );
+			$this->assertStringContainsString( 'data-wp-text="context.item.count">1</span>', $html );
+
+			$_SERVER['REQUEST_URI'] = '/?wcp-0-categories=local-red&min_price=999';
+			$html                   = do_blocks( $markup );
+			$this->assertStringContainsString( 'wp-block-post-title', $html );
+			$this->assertStringContainsString( 'data-wp-text="context.item.count">2</span>', $html );
+		} finally {
+			if ( null === $original_uri ) {
+				unset( $_SERVER['REQUEST_URI'] );
+			} else {
+				$_SERVER['REQUEST_URI'] = $original_uri;
+			}
+		}
+	}
+
 	/**
 	 * Set up the test subject and dependencies.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/ParamsTest.php b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/ParamsTest.php
index 6f9ed3a1d38..4e2656d7ca1 100644
--- a/plugins/woocommerce/tests/php/src/Internal/ProductFilters/ParamsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/ProductFilters/ParamsTest.php
@@ -32,6 +32,39 @@ class ParamsTest extends AbstractProductFiltersTest {
 	 */
 	private $taxonomy_params_filter;

+	/**
+	 * @testdox Should isolate local filter parameters, including ID zero, and reject malformed values.
+	 */
+	public function test_scoped_request_parameters(): void {
+		$this->taxonomy_params_filter = static function ( $params ) {
+			$params['taxonomy']['product_brand'] = 'makers';
+			return $params;
+		};
+		add_filter( 'woocommerce_product_filter_params', $this->taxonomy_params_filter );
+
+		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Restore the original request verbatim after the test.
+		$original_uri = $_SERVER['REQUEST_URI'] ?? null;
+		try {
+			$_SERVER['REQUEST_URI'] = '/?min_price=5&wcp-0-min_price=10.28&wcp-17-min_price=20&wcp-0-rating_filter[]=4&wcp-0-max_price=invalid&wcp-0-unknown=value&wcp-0-filter_color=blue,red&wcp-0-query_type_color=and&wcp-00-min_price=999&wcp-0-makers=%25e6%2597%25a5';
+			$local                  = $this->sut->get_filter_params( 0 );
+			$this->assertSame( '%e6%97%a5', $local['makers'] );
+			$this->assertSame( '10.28', $local['min_price'] );
+			$this->assertSame( 'blue,red', $local['filter_color'] );
+			$this->assertSame( 'and', $local['query_type_color'] );
+			$this->assertArrayNotHasKey( 'rating_filter', $local );
+			$this->assertArrayNotHasKey( 'max_price', $local );
+			$this->assertArrayNotHasKey( 'unknown', $local );
+			$this->assertSame( '20', $this->sut->get_filter_params( 17 )['min_price'] );
+			$this->assertSame( '5', $this->sut->get_filter_params()['min_price'] );
+		} finally {
+			if ( null === $original_uri ) {
+				unset( $_SERVER['REQUEST_URI'] );
+			} else {
+				$_SERVER['REQUEST_URI'] = $original_uri;
+			}
+		}
+	}
+
 	/**
 	 * Runs before each test.
 	 */