Commit b779d7a0431 for woocommerce

commit b779d7a043104f14814f6ab7a8a2ec7e0e264b5e
Author: Karol Manijak <20098064+kmanijak@users.noreply.github.com>
Date:   Tue Jul 21 14:27:39 2026 +0200

    Make Core Breadcrumbs compatible with WooCommerce Store Breadcrumbs (#66382)

    * Fix Core Breadcrumbs WooCommerce compatibility

    * Simplify Core Breadcrumbs compatibility helpers

    * Simplify Core Breadcrumbs compatibility scope

    * Stabilize paginated product search breadcrumb test

    * Make product search breadcrumb test deterministic

    * Limit Core Breadcrumbs compatibility to Woo contexts

    * Restore search and legacy breadcrumb filter coverage

    * Restore global breadcrumb home URL filter

    * Remove redundant breadcrumb compatibility tests

    * Fix Core breadcrumb visibility compatibility

    * Fix Core breadcrumbs account test isolation

diff --git a/plugins/woocommerce/changelog/fix-core-breadcrumbs-compatibility b/plugins/woocommerce/changelog/fix-core-breadcrumbs-compatibility
new file mode 100644
index 00000000000..f73bcfbc0d4
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-core-breadcrumbs-compatibility
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Align the Core Breadcrumbs block output with WooCommerce Store Breadcrumbs on shop, product, account, archive, and custom taxonomy pages.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypesController.php b/plugins/woocommerce/src/Blocks/BlockTypesController.php
index db68a52932b..e148140a453 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypesController.php
@@ -63,8 +63,6 @@ final class BlockTypesController {
 		add_action( 'woocommerce_login_form_end', array( $this, 'redirect_to_field' ) );
 		add_filter( 'widget_types_to_hide_from_legacy_widget_block', array( $this, 'hide_legacy_widgets_with_block_equivalent' ) );
 		add_filter( 'register_block_type_args', array( $this, 'enqueue_block_style_for_classic_themes' ), 10, 2 );
-		add_filter( 'block_core_breadcrumbs_post_type_settings', array( $this, 'set_product_breadcrumbs_preferred_taxonomy' ), 10, 3 );
-		add_filter( 'block_core_breadcrumbs_items', array( $this, 'apply_woocommerce_breadcrumb_filters' ), 10, 1 );
 	}

 	/**
@@ -583,114 +581,4 @@ final class BlockTypesController {

 		return $args;
 	}
-
-	/**
-	 * Set the preferred taxonomy and term for the breadcrumbs block on the product post type.
-	 *
-	 * This method mimics the behavior of WC_Breadcrumb::add_crumbs_single() to ensure
-	 * consistent breadcrumb term selection between WooCommerce's legacy breadcrumbs
-	 * and the Core breadcrumbs block.
-	 *
-	 * @param array  $settings The settings for the breadcrumbs block.
-	 * @param string $post_type The post type.
-	 * @param int    $post_id The current post ID.
-	 * @return array The settings for the breadcrumbs block.
-	 *
-	 * @internal
-	 */
-	public function set_product_breadcrumbs_preferred_taxonomy( $settings, $post_type, $post_id = 0 ) {
-		if ( ! is_array( $settings ) || 'product' !== $post_type ) {
-			return $settings;
-		}
-
-		$settings['taxonomy'] = 'product_cat';
-
-		// If we have a post ID, determine the specific term using WooCommerce's logic.
-		if ( ! empty( $post_id ) ) {
-			$terms = wc_get_product_terms(
-				$post_id,
-				'product_cat',
-				/**
-				 * Filters the arguments used to fetch product terms for breadcrumbs.
-				 *
-				 * @since 9.5.0
-				 *
-				 * @param array $args Array of arguments for `wc_get_product_terms()`.
-				 */
-				apply_filters(
-					'woocommerce_breadcrumb_product_terms_args',
-					array(
-						'orderby' => 'parent',
-						'order'   => 'DESC',
-					)
-				)
-			);
-
-			if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
-				/**
-				 * Filters the main term used in product breadcrumbs.
-				 *
-				 * @since 9.5.0
-				 *
-				 * @param \WP_Term   $main_term The main term to be used in breadcrumbs.
-				 * @param \WP_Term[] $terms     Array of all product category terms.
-				 */
-				$main_term = apply_filters( 'woocommerce_breadcrumb_main_term', $terms[0], $terms );
-
-				if ( $main_term instanceof \WP_Term ) {
-					$settings['term'] = $main_term->slug;
-				}
-			}
-		}
-
-		return $settings;
-	}
-
-	/**
-	 * Apply WooCommerce breadcrumb filters to Core breadcrumbs block items.
-	 *
-	 * This bridges the Core breadcrumbs block with WooCommerce's legacy breadcrumb filters,
-	 * ensuring backward compatibility for sites that have customized breadcrumbs using
-	 * the `woocommerce_get_breadcrumb` filter.
-	 *
-	 * @param array $items Array of breadcrumb items from Core.
-	 * @return array Modified breadcrumb items.
-	 *
-	 * @internal
-	 */
-	public function apply_woocommerce_breadcrumb_filters( $items ) {
-		// Convert Core format to WooCommerce format.
-		// Core: array( 'url' => '...', 'label' => '...' )
-		// Woo: array( 'label', 'url' ).
-		$wc_crumbs = array_map(
-			function ( $item ) {
-				return array(
-					$item['label'] ?? '',
-					$item['url'] ?? '',
-				);
-			},
-			$items
-		);
-
-		/**
-		 * Filters the breadcrumb trail array.
-		 *
-		 * @since 2.3.0
-		 *
-		 * @param array         $crumbs The breadcrumb trail.
-		 * @param \WC_Breadcrumb|null $breadcrumb The breadcrumb object (null when called from Core block).
-		 */
-		$wc_crumbs = apply_filters( 'woocommerce_get_breadcrumb', $wc_crumbs, null );
-
-		// Convert back to Core format.
-		return array_map(
-			function ( $crumb ) {
-				return array(
-					'label' => $crumb[0] ?? '',
-					'url'   => $crumb[1] ?? '',
-				);
-			},
-			$wc_crumbs
-		);
-	}
 }
diff --git a/plugins/woocommerce/src/Blocks/CoreBreadcrumbsCompatibility.php b/plugins/woocommerce/src/Blocks/CoreBreadcrumbsCompatibility.php
new file mode 100644
index 00000000000..8a750e28f0e
--- /dev/null
+++ b/plugins/woocommerce/src/Blocks/CoreBreadcrumbsCompatibility.php
@@ -0,0 +1,647 @@
+<?php
+declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Blocks;
+
+/**
+ * Adds WooCommerce compatibility behavior to the Core Breadcrumbs block.
+ *
+ * @internal
+ */
+final class CoreBreadcrumbsCompatibility {
+
+	/**
+	 * Whether the compatibility hooks have been initialized.
+	 *
+	 * @var bool
+	 */
+	private $is_initialized = false;
+
+	/**
+	 * Initialize Core Breadcrumbs compatibility hooks.
+	 *
+	 * @internal
+	 */
+	public function init(): void {
+		if ( $this->is_initialized ) {
+			return;
+		}
+
+		add_filter( 'block_core_breadcrumbs_post_type_settings', array( $this, 'set_product_breadcrumbs_preferred_taxonomy' ), 10, 3 );
+		add_filter( 'block_core_breadcrumbs_items', array( $this, 'apply_woocommerce_breadcrumb_filters' ), 10, 1 );
+
+		$this->is_initialized = true;
+	}
+
+	/*
+	 * Compatibility methods.
+	 */
+
+	/**
+	 * Set the preferred taxonomy and term for the breadcrumbs block on the product post type.
+	 *
+	 * This method mimics the behavior of WC_Breadcrumb::add_crumbs_single() to ensure
+	 * consistent breadcrumb term selection between WooCommerce's legacy breadcrumbs
+	 * and the Core breadcrumbs block.
+	 *
+	 * @internal
+	 *
+	 * @param array  $settings The settings for the breadcrumbs block.
+	 * @param string $post_type The post type.
+	 * @param int    $post_id The current post ID.
+	 * @return array The settings for the breadcrumbs block.
+	 */
+	public function set_product_breadcrumbs_preferred_taxonomy( $settings, $post_type, $post_id = 0 ) {
+		if ( ! is_array( $settings ) || 'product' !== $post_type ) {
+			return $settings;
+		}
+
+		$settings['taxonomy'] = 'product_cat';
+		$post_id              = (int) $post_id;
+
+		if ( ! $post_id ) {
+			return $settings;
+		}
+
+		$terms = wc_get_product_terms(
+			$post_id,
+			'product_cat',
+			/**
+			 * Filters the arguments used to fetch product terms for breadcrumbs.
+			 *
+			 * @since 9.5.0
+			 *
+			 * @param array $args Array of arguments for `wc_get_product_terms()`.
+			 */
+			apply_filters(
+				'woocommerce_breadcrumb_product_terms_args',
+				array(
+					'orderby' => 'parent',
+					'order'   => 'DESC',
+				)
+			)
+		);
+
+		if ( empty( $terms ) || is_wp_error( $terms ) ) {
+			return $settings;
+		}
+
+		/**
+		 * Filters the main term used in product breadcrumbs.
+		 *
+		 * @since 9.5.0
+		 *
+		 * @param \WP_Term   $main_term The main term to be used in breadcrumbs.
+		 * @param \WP_Term[] $terms     Array of all product category terms.
+		 */
+		$main_term = apply_filters( 'woocommerce_breadcrumb_main_term', $terms[0], $terms );
+
+		if ( $main_term instanceof \WP_Term ) {
+			$settings['term'] = $main_term->slug;
+		}
+
+		return $settings;
+	}
+
+	/**
+	 * Apply WooCommerce breadcrumb filters to Core breadcrumbs block items.
+	 *
+	 * This bridges the Core breadcrumbs block with WooCommerce's legacy breadcrumb filters,
+	 * ensuring backward compatibility for sites that have customized breadcrumbs using
+	 * the `woocommerce_get_breadcrumb` filter.
+	 *
+	 * @internal
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	public function apply_woocommerce_breadcrumb_filters( $items ) {
+		if ( ! is_array( $items ) ) {
+			return $items;
+		}
+
+		$items = $this->apply_woocommerce_core_breadcrumb_adjustments( $items );
+
+		if ( ! has_filter( 'woocommerce_get_breadcrumb' ) ) {
+			return $items;
+		}
+
+		// Convert Core format to WooCommerce format.
+		// Core: array( 'url' => '...', 'label' => '...' )
+		// Woo: array( 'label', 'url' ).
+		$wc_crumbs = array_map(
+			function ( $item ) {
+				return array(
+					$item['label'] ?? '',
+					$item['url'] ?? '',
+				);
+			},
+			$items
+		);
+
+		/**
+		 * Filters the breadcrumb trail array.
+		 *
+		 * @since 2.3.0
+		 *
+		 * @param array               $crumbs The breadcrumb trail.
+		 * @param \WC_Breadcrumb|null $breadcrumb The breadcrumb object (null when called from Core block).
+		 */
+		$wc_crumbs = apply_filters( 'woocommerce_get_breadcrumb', $wc_crumbs, null );
+
+		$core_items = array();
+
+		foreach ( $wc_crumbs as $index => $crumb ) {
+			$item  = isset( $items[ $index ] ) && is_array( $items[ $index ] ) ? $items[ $index ] : array();
+			$label = is_array( $crumb ) ? ( $crumb[0] ?? '' ) : '';
+			$url   = is_array( $crumb ) ? ( $crumb[1] ?? '' ) : '';
+
+			$item['label'] = $label;
+
+			if ( $url ) {
+				$item['url'] = $url;
+			} else {
+				unset( $item['url'] );
+			}
+
+			$core_items[] = $item;
+		}
+
+		return $core_items;
+	}
+
+	/**
+	 * Apply WooCommerce breadcrumb behavior to Core breadcrumbs.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function apply_woocommerce_core_breadcrumb_adjustments( $items ) {
+		if ( ! is_array( $items ) ) {
+			return $items;
+		}
+
+		$items = $this->replace_product_archive_breadcrumb_label( $items );
+		$items = $this->prepend_shop_page_to_product_taxonomy_breadcrumbs( $items );
+		$items = $this->prepend_taxonomy_label_to_product_taxonomy_breadcrumbs( $items );
+		$items = $this->prepend_shop_page_to_product_search_breadcrumbs( $items );
+		$items = $this->replace_product_tag_breadcrumb_label( $items );
+		$items = $this->replace_search_breadcrumb_label( $items );
+		$items = $this->prepend_my_account_page_to_endpoint_breadcrumbs( $items );
+		$items = $this->apply_home_breadcrumb_url_filter( $items );
+
+		return $items;
+	}
+
+	/**
+	 * Apply WooCommerce's Home breadcrumb URL filter.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function apply_home_breadcrumb_url_filter( $items ) {
+		if ( ! has_filter( 'woocommerce_breadcrumb_home_url' ) ) {
+			return $items;
+		}
+
+		$home_index = $this->get_breadcrumb_item_index_by_url( $items, home_url( '/' ) );
+
+		if ( null === $home_index ) {
+			return $items;
+		}
+
+		$default_home_url = home_url();
+
+		/**
+		 * Filters the Home breadcrumb URL.
+		 *
+		 * @param string $url The Home breadcrumb URL.
+		 *
+		 * @since 2.3.0
+		 */
+		$home_url = apply_filters( 'woocommerce_breadcrumb_home_url', $default_home_url );
+
+		$items[ $home_index ]['url'] = is_string( $home_url ) ? $home_url : $default_home_url;
+
+		return $items;
+	}
+
+	/**
+	 * Replace product archive breadcrumb labels with the WooCommerce shop page title.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function replace_product_archive_breadcrumb_label( $items ) {
+		$shop_url = get_post_type_archive_link( 'product' );
+
+		if ( ! $shop_url || null === $this->get_breadcrumb_item_index_by_url( $items, $shop_url ) ) {
+			return $items;
+		}
+
+		$shop_page_item = $this->get_shop_page_breadcrumb_item( $shop_url );
+
+		if ( empty( $shop_page_item ) ) {
+			return $items;
+		}
+
+		foreach ( $items as $index => $item ) {
+			if ( self::are_breadcrumb_urls_equal( $item['url'] ?? '', $shop_page_item['url'] ) ) {
+				$items[ $index ]['label'] = $shop_page_item['label'];
+			}
+		}
+
+		return $items;
+	}
+
+	/**
+	 * Prepend the shop page to product taxonomy breadcrumbs.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function prepend_shop_page_to_product_taxonomy_breadcrumbs( $items ) {
+		if ( ! ( is_product_category() || is_product_tag() ) ) {
+			return $items;
+		}
+
+		$permalinks = wc_get_permalink_structure();
+		$shop_page  = $this->get_woocommerce_page_post( 'shop' );
+
+		if (
+			! $shop_page ||
+			! isset( $permalinks['product_base'] ) ||
+			! strstr( $permalinks['product_base'], '/' . $shop_page->post_name ) ||
+			intval( get_option( 'page_on_front' ) ) === $shop_page->ID
+		) {
+			return $items;
+		}
+
+		return $this->prepend_shop_page_to_breadcrumbs( $items );
+	}
+
+	/**
+	 * Prepend taxonomy labels to product taxonomy breadcrumbs.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function prepend_taxonomy_label_to_product_taxonomy_breadcrumbs( $items ) {
+		if ( ! is_product_taxonomy() || empty( $items ) ) {
+			return $items;
+		}
+
+		$current_term = $this->get_queried_term();
+
+		if ( ! $current_term || in_array( $current_term->taxonomy, array( 'product_brand', 'product_cat', 'product_tag' ), true ) ) {
+			return $items;
+		}
+
+		$taxonomy = get_taxonomy( $current_term->taxonomy );
+
+		if ( ! $taxonomy || ! $taxonomy->labels->name ) {
+			return $items;
+		}
+
+		$insert_index = $this->get_first_breadcrumb_insert_index( $items );
+
+		if ( isset( $items[ $insert_index ]['label'] ) && $taxonomy->labels->name === $items[ $insert_index ]['label'] ) {
+			return $items;
+		}
+
+		return $this->insert_parent_breadcrumb_item(
+			$items,
+			array(
+				'label' => $taxonomy->labels->name,
+			)
+		);
+	}
+
+	/**
+	 * Prepend the shop page to product search breadcrumbs.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function prepend_shop_page_to_product_search_breadcrumbs( $items ) {
+		if ( ! $this->is_product_search() || intval( get_option( 'page_on_front' ) ) === wc_get_page_id( 'shop' ) ) {
+			return $items;
+		}
+
+		return $this->prepend_shop_page_to_breadcrumbs( $items );
+	}
+
+	/**
+	 * Replace Core's search breadcrumb label with WooCommerce's search label.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function replace_search_breadcrumb_label( $items ) {
+		if ( ! is_search() || empty( $items ) ) {
+			return $items;
+		}
+
+		$search_url = (int) get_query_var( 'paged' ) > 1 ? get_pagenum_link( 1 ) : '';
+
+		/* translators: %s: search term */
+		return $this->replace_current_archive_breadcrumb_label( $items, sprintf( __( 'Search results for &ldquo;%s&rdquo;', 'woocommerce' ), get_search_query() ), $search_url );
+	}
+
+	/**
+	 * Replace product tag breadcrumb labels with WooCommerce's tag archive label.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function replace_product_tag_breadcrumb_label( $items ) {
+		if ( ! is_product_tag() || empty( $items ) ) {
+			return $items;
+		}
+
+		$current_term = $this->get_queried_term();
+
+		if ( ! $current_term ) {
+			return $items;
+		}
+
+		$tag_link = get_term_link( $current_term, 'product_tag' );
+
+		if ( is_wp_error( $tag_link ) ) {
+			$tag_link = '';
+		}
+
+		/* translators: %s: product tag */
+		return $this->replace_current_archive_breadcrumb_label( $items, sprintf( __( 'Products tagged &ldquo;%s&rdquo;', 'woocommerce' ), $current_term->name ), $tag_link );
+	}
+
+	/**
+	 * Prepend the My Account page to account endpoint breadcrumbs.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function prepend_my_account_page_to_endpoint_breadcrumbs( $items ) {
+		if ( ! is_wc_endpoint_url() || ! is_account_page() ) {
+			return $items;
+		}
+
+		$my_account_page = $this->get_woocommerce_page_post( 'myaccount' );
+		$my_account_url  = $my_account_page ? get_permalink( $my_account_page ) : '';
+
+		if ( ! $my_account_page || ! $my_account_url ) {
+			return $items;
+		}
+
+		$woocommerce = WC();
+
+		if ( $woocommerce->query instanceof \WC_Query ) {
+			$endpoint       = $woocommerce->query->get_current_endpoint();
+			$action         = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Used only to select the breadcrumb label.
+			$endpoint_title = $endpoint ? $woocommerce->query->get_endpoint_title( $endpoint, $action ) : '';
+
+			if ( $endpoint_title ) {
+				$items = $this->replace_current_archive_breadcrumb_label( $items, $endpoint_title );
+			}
+		}
+
+		return $this->insert_parent_breadcrumb_item_if_missing_url(
+			$items,
+			array(
+				'label' => $my_account_page->post_title,
+				'url'   => $my_account_url,
+			)
+		);
+	}
+
+	/*
+	 * Utility methods.
+	 */
+
+	/**
+	 * Check whether the current request is a product search.
+	 *
+	 * @return bool Whether the current request is a product search.
+	 */
+	private function is_product_search(): bool {
+		if ( ! is_search() ) {
+			return false;
+		}
+
+		$post_type = get_query_var( 'post_type' );
+
+		if ( is_array( $post_type ) ) {
+			return in_array( 'product', $post_type, true );
+		}
+
+		return 'product' === $post_type || is_shop();
+	}
+
+	/**
+	 * Prepend the shop page to breadcrumb items.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function prepend_shop_page_to_breadcrumbs( $items ) {
+		$shop_page_item = $this->get_shop_page_breadcrumb_item();
+
+		if ( empty( $shop_page_item ) ) {
+			return $items;
+		}
+
+		$shop_page_index = $this->get_breadcrumb_item_index_by_url( $items, $shop_page_item['url'] );
+
+		if ( null !== $shop_page_index ) {
+			return $this->replace_breadcrumb_label_at_index( $items, $shop_page_index, $shop_page_item['label'] );
+		}
+
+		return $this->insert_parent_breadcrumb_item_if_missing_url( $items, $shop_page_item );
+	}
+
+	/**
+	 * Get the shop page breadcrumb item.
+	 *
+	 * @param string $shop_url Shop archive URL.
+	 * @return array|null Shop page breadcrumb item.
+	 */
+	private function get_shop_page_breadcrumb_item( $shop_url = '' ) {
+		$shop_page  = $this->get_woocommerce_page_post( 'shop' );
+		$shop_url   = $shop_url ? $shop_url : (
+			$shop_page ? get_permalink( $shop_page ) : get_post_type_archive_link( 'product' )
+		);
+		$shop_label = $shop_page ? get_the_title( $shop_page ) : '';
+
+		if ( ! $shop_label ) {
+			$product_post_type = get_post_type_object( 'product' );
+			$shop_label        = $product_post_type ? $product_post_type->labels->name : '';
+		}
+
+		if ( ! $shop_url || ! $shop_label ) {
+			return null;
+		}
+
+		return array(
+			'label' => $shop_label,
+			'url'   => $shop_url,
+		);
+	}
+
+	/**
+	 * Get a WooCommerce page post.
+	 *
+	 * @param string $page_name WooCommerce page name.
+	 * @return \WP_Post|null WooCommerce page post.
+	 */
+	private function get_woocommerce_page_post( string $page_name ) {
+		$page_id = wc_get_page_id( $page_name );
+
+		return $page_id > 0 ? get_post( $page_id ) : null;
+	}
+
+	/**
+	 * Get the queried term.
+	 *
+	 * @return \WP_Term|null Queried term.
+	 */
+	private function get_queried_term() {
+		$queried_object = $GLOBALS['wp_query']->get_queried_object();
+
+		return $queried_object instanceof \WP_Term ? $queried_object : null;
+	}
+
+	/**
+	 * Replace the current archive breadcrumb label.
+	 *
+	 * @param array  $items Array of breadcrumb items from Core.
+	 * @param string $label Replacement label.
+	 * @param string $archive_url Archive URL.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function replace_current_archive_breadcrumb_label( $items, $label, $archive_url = '' ) {
+		$item_index = $this->get_breadcrumb_item_index_by_url( $items, $archive_url );
+
+		if ( null === $item_index ) {
+			$item_keys = array_keys( $items );
+
+			if ( empty( $item_keys ) ) {
+				return $items;
+			}
+
+			$item_index = end( $item_keys );
+			$paged      = (int) get_query_var( 'paged' );
+
+			if ( $paged > 1 && count( $item_keys ) > 1 && isset( $items[ $item_index ]['label'] ) ) {
+				$pagination_label = sprintf(
+					/* translators: %s: page number */
+					__( 'Page %s', 'default' ), // phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Match the Core Breadcrumbs block pagination label.
+					number_format_i18n( $paged )
+				);
+
+				if ( $pagination_label === (string) $items[ $item_index ]['label'] ) {
+					$item_index = $item_keys[ count( $item_keys ) - 2 ];
+				}
+			}
+
+			if ( ! empty( $items[ $item_index ]['url'] ) ) {
+				return $items;
+			}
+		}
+
+		return $this->replace_breadcrumb_label_at_index( $items, $item_index, $label );
+	}
+
+	/**
+	 * Replace a breadcrumb label at an index.
+	 *
+	 * @param array           $items Array of breadcrumb items from Core.
+	 * @param int|string|null $index Breadcrumb item index.
+	 * @param string          $label Replacement label.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function replace_breadcrumb_label_at_index( $items, $index, $label ) {
+		if ( null === $index || ! isset( $items[ $index ] ) ) {
+			return $items;
+		}
+
+		$items[ $index ]['label'] = $label;
+
+		return $items;
+	}
+
+	/**
+	 * Insert a parent breadcrumb item if its URL is not already present.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @param array $item Breadcrumb item to insert.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function insert_parent_breadcrumb_item_if_missing_url( $items, $item ) {
+		if ( empty( $item['url'] ) || null !== $this->get_breadcrumb_item_index_by_url( $items, $item['url'] ) ) {
+			return $items;
+		}
+
+		return $this->insert_parent_breadcrumb_item( $items, $item );
+	}
+
+	/**
+	 * Insert a parent breadcrumb item after the home item.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @param array $item Breadcrumb item to insert.
+	 * @return array Modified breadcrumb items.
+	 */
+	private function insert_parent_breadcrumb_item( $items, $item ) {
+		array_splice( $items, $this->get_first_breadcrumb_insert_index( $items ), 0, array( $item ) );
+
+		return $items;
+	}
+
+	/**
+	 * Get the first breadcrumb item index matching a URL.
+	 *
+	 * @param array  $items Array of breadcrumb items from Core.
+	 * @param string $url URL to find.
+	 * @return int|string|null Breadcrumb item index.
+	 */
+	private function get_breadcrumb_item_index_by_url( $items, $url ) {
+		if ( ! $url ) {
+			return null;
+		}
+
+		foreach ( $items as $index => $item ) {
+			if ( self::are_breadcrumb_urls_equal( $item['url'] ?? '', $url ) ) {
+				return $index;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Get the index where WooCommerce should insert parent breadcrumb items.
+	 *
+	 * @param array $items Array of breadcrumb items from Core.
+	 * @return int Breadcrumb insertion index.
+	 */
+	private function get_first_breadcrumb_insert_index( $items ) {
+		if ( empty( $items ) ) {
+			return 0;
+		}
+
+		$first_item = reset( $items );
+
+		return self::are_breadcrumb_urls_equal( $first_item['url'] ?? '', home_url( '/' ) ) ? 1 : 0;
+	}
+
+	/**
+	 * Check whether two breadcrumb URLs are equivalent.
+	 *
+	 * @param string $first_url First URL.
+	 * @param string $second_url Second URL.
+	 * @return bool Whether the URLs are equivalent.
+	 */
+	private static function are_breadcrumb_urls_equal( $first_url, $second_url ) {
+		return untrailingslashit( (string) $first_url ) === untrailingslashit( (string) $second_url );
+	}
+}
diff --git a/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php b/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
index 9975adbf51b..86609eb3c24 100644
--- a/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
+++ b/plugins/woocommerce/src/Blocks/Domain/Bootstrap.php
@@ -9,6 +9,7 @@ use Automattic\WooCommerce\Blocks\BlockPatterns;
 use Automattic\WooCommerce\Blocks\BlockTemplatesRegistry;
 use Automattic\WooCommerce\Blocks\BlockTemplatesController;
 use Automattic\WooCommerce\Blocks\BlockTypesController;
+use Automattic\WooCommerce\Blocks\CoreBreadcrumbsCompatibility;
 use Automattic\WooCommerce\Blocks\DependencyDetection;
 use Automattic\WooCommerce\Blocks\Patterns\PatternRegistry;
 use Automattic\WooCommerce\Blocks\Patterns\PTKClient;
@@ -149,6 +150,7 @@ class Bootstrap {
 			if ( ( new BlockRegistrationContext() )->should_register() ) {
 				$this->container->get( BlockPatterns::class );
 				$this->container->get( BlockTypesController::class );
+				$this->container->get( CoreBreadcrumbsCompatibility::class )->init();
 			}
 			$this->container->get( ClassicTemplatesCompatibility::class );
 			$this->container->get( Notices::class )->init();
@@ -245,6 +247,12 @@ class Bootstrap {
 				return new BlockTypesController( $asset_api, $asset_data_registry );
 			}
 		);
+		$this->container->register(
+			CoreBreadcrumbsCompatibility::class,
+			function () {
+				return new CoreBreadcrumbsCompatibility();
+			}
+		);
 		$this->container->register(
 			ClassicTemplatesCompatibility::class,
 			function ( Container $container ) {
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
index b75082a26fd..6edfde59d5f 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypesController.php
@@ -5,13 +5,14 @@ namespace Automattic\WooCommerce\Tests\Blocks;

 use Automattic\WooCommerce\Blocks\Assets\Api;
 use Automattic\WooCommerce\Blocks\BlockTypesController as TestedBlockTypesController;
-use Automattic\WooCommerce\Tests\Blocks\Mocks\AssetDataRegistryMock;
 use Automattic\WooCommerce\Blocks\Package;
+use Automattic\WooCommerce\Tests\Blocks\Mocks\AssetDataRegistryMock;
+use WC_Unit_Test_Case;

 /**
- * Unit tests for the PatternRegistry class.
+ * Unit tests for the BlockTypesController class.
  */
-class BlockTypesController extends \WP_UnitTestCase {
+class BlockTypesController extends WC_Unit_Test_Case {

 	/**
 	 * Holds the BlockTypesController under test.
@@ -26,8 +27,9 @@ class BlockTypesController extends \WP_UnitTestCase {
 	 * @return void
 	 * @throws \Exception If there is no dependency for the given identifier in the container the setup will fail.
 	 */
-	protected function setUp(): void {
+	public function setUp(): void {
 		parent::setUp();
+
 		$this->block_types_controller = new TestedBlockTypesController(
 			Package::container()->get( Api::class ),
 			new AssetDataRegistryMock( Package::container()->get( API::class ) )
@@ -35,12 +37,9 @@ class BlockTypesController extends \WP_UnitTestCase {
 	}

 	/**
-	 * Register 3 blocks, one will be allowed by full name, one by namespace,and one because it has a parent with a
-	 * woocommerce namespace.
-	 *
-	 * @return void
+	 * @testdox Should identify blocks that should have data attributes.
 	 */
-	public function test_block_should_have_data_attributes() {
+	public function test_block_should_have_data_attributes(): void {

 		// A block that will not be allowed data attributes.
 		register_block_type(
diff --git a/plugins/woocommerce/tests/php/src/Blocks/CoreBreadcrumbsCompatibilityTest.php b/plugins/woocommerce/tests/php/src/Blocks/CoreBreadcrumbsCompatibilityTest.php
new file mode 100644
index 00000000000..655dd0da9ed
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/CoreBreadcrumbsCompatibilityTest.php
@@ -0,0 +1,870 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks;
+
+use Automattic\WooCommerce\Blocks\CoreBreadcrumbsCompatibility;
+use Automattic\WooCommerce\Blocks\Domain\Bootstrap;
+use Automattic\WooCommerce\Blocks\Domain\Package as BlocksPackage;
+use Automattic\WooCommerce\Blocks\Registry\Container;
+use WC_Unit_Test_Case;
+
+/**
+ * Unit tests for the CoreBreadcrumbsCompatibility class.
+ */
+class CoreBreadcrumbsCompatibilityTest extends WC_Unit_Test_Case {
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var CoreBreadcrumbsCompatibility
+	 */
+	private CoreBreadcrumbsCompatibility $sut;
+
+	/**
+	 * Original shop page ID.
+	 *
+	 * @var mixed
+	 */
+	private $original_shop_page_id;
+
+	/**
+	 * Original My Account page ID.
+	 *
+	 * @var mixed
+	 */
+	private $original_myaccount_page_id;
+
+	/**
+	 * Original WooCommerce permalinks.
+	 *
+	 * @var mixed
+	 */
+	private $original_woocommerce_permalinks;
+
+	/**
+	 * Original product post type archive setting.
+	 *
+	 * @var mixed
+	 */
+	private $original_product_has_archive;
+
+	/**
+	 * Original WooCommerce query object.
+	 *
+	 * @var \WC_Query
+	 */
+	private \WC_Query $original_woocommerce_query;
+
+	/**
+	 * Original request query vars.
+	 *
+	 * @var array
+	 */
+	private $original_wp_query_vars;
+
+	/**
+	 * Shop page ID.
+	 *
+	 * @var int
+	 */
+	private $shop_page_id;
+
+	/**
+	 * My Account page ID.
+	 *
+	 * @var int
+	 */
+	private $my_account_page_id;
+
+	/**
+	 * Sets up test fixtures.
+	 *
+	 * @return void
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		global $wp;
+
+		$this->sut = new CoreBreadcrumbsCompatibility();
+
+		$product_post_type = get_post_type_object( 'product' );
+
+		$this->original_shop_page_id            = get_option( 'woocommerce_shop_page_id' );
+		$this->original_myaccount_page_id       = get_option( 'woocommerce_myaccount_page_id' );
+		$this->original_woocommerce_permalinks  = get_option( 'woocommerce_permalinks' );
+		$this->original_product_has_archive     = $product_post_type ? $product_post_type->has_archive : null;
+		$this->original_woocommerce_query       = WC()->query;
+		$this->original_wp_query_vars           = $wp instanceof \WP && is_array( $wp->query_vars ) ? $wp->query_vars : array();
+		$this->shop_page_id                     = self::factory()->post->create(
+			array(
+				'post_type'   => 'page',
+				'post_status' => 'publish',
+				'post_title'  => 'Catalog',
+				'post_name'   => 'shop',
+			)
+		);
+		$this->my_account_page_id               = self::factory()->post->create(
+			array(
+				'post_type'   => 'page',
+				'post_status' => 'publish',
+				'post_title'  => 'My account',
+				'post_name'   => 'my-account',
+			)
+		);
+		$woocommerce_permalinks                 = is_array( $this->original_woocommerce_permalinks ) ? $this->original_woocommerce_permalinks : array();
+		$woocommerce_permalinks['product_base'] = '/shop';
+
+		update_option( 'woocommerce_shop_page_id', $this->shop_page_id );
+		update_option( 'woocommerce_myaccount_page_id', $this->my_account_page_id );
+		update_option( 'woocommerce_permalinks', $woocommerce_permalinks );
+
+		if ( $product_post_type ) {
+			$product_post_type->has_archive = get_page_uri( $this->shop_page_id );
+		}
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 *
+	 * @return void
+	 */
+	public function tearDown(): void {
+		global $wp;
+
+		remove_filter( 'block_core_breadcrumbs_post_type_settings', array( $this->sut, 'set_product_breadcrumbs_preferred_taxonomy' ), 10 );
+		remove_filter( 'block_core_breadcrumbs_items', array( $this->sut, 'apply_woocommerce_breadcrumb_filters' ), 10 );
+		remove_filter( 'woocommerce_is_account_page', '__return_true' );
+
+		update_option( 'woocommerce_shop_page_id', $this->original_shop_page_id );
+		update_option( 'woocommerce_myaccount_page_id', $this->original_myaccount_page_id );
+		update_option( 'woocommerce_permalinks', $this->original_woocommerce_permalinks );
+
+		$product_post_type = get_post_type_object( 'product' );
+		if ( $product_post_type ) {
+			$product_post_type->has_archive = $this->original_product_has_archive;
+		}
+
+		if ( $wp instanceof \WP ) {
+			$wp->query_vars = $this->original_wp_query_vars;
+		}
+
+		WC()->query = $this->original_woocommerce_query;
+
+		if ( taxonomy_exists( 'pa_test_color' ) ) {
+			unregister_taxonomy( 'pa_test_color' );
+		}
+
+		parent::tearDown();
+	}
+
+	/**
+	 * @testdox Should register Core Breadcrumbs compatibility filters.
+	 */
+	public function test_init_registers_core_breadcrumbs_filters(): void {
+		$this->sut->init();
+		$this->sut->init();
+
+		$this->assertSame( 10, has_filter( 'block_core_breadcrumbs_post_type_settings', array( $this->sut, 'set_product_breadcrumbs_preferred_taxonomy' ) ), 'Product breadcrumb settings filter should be registered.' );
+		$this->assertSame( 10, has_filter( 'block_core_breadcrumbs_items', array( $this->sut, 'apply_woocommerce_breadcrumb_filters' ) ), 'Breadcrumb items filter should be registered.' );
+	}
+
+	/**
+	 * @testdox Should register Core Breadcrumbs compatibility filters through the Blocks bootstrap.
+	 */
+	public function test_blocks_bootstrap_registers_core_breadcrumbs_filters(): void {
+		$container = new Container();
+		$container->register(
+			BlocksPackage::class,
+			function () {
+				return new BlocksPackage( 'test', dirname( WC_PLUGIN_FILE ) );
+			}
+		);
+
+		new Bootstrap( $container );
+		$compatibility = $container->get( CoreBreadcrumbsCompatibility::class );
+
+		try {
+			$this->assertSame( 10, has_filter( 'block_core_breadcrumbs_post_type_settings', array( $compatibility, 'set_product_breadcrumbs_preferred_taxonomy' ) ), 'Bootstrap should register the product breadcrumb settings filter.' );
+			$this->assertSame( 10, has_filter( 'block_core_breadcrumbs_items', array( $compatibility, 'apply_woocommerce_breadcrumb_filters' ) ), 'Bootstrap should register the breadcrumb items filter.' );
+		} finally {
+			remove_filter( 'block_core_breadcrumbs_post_type_settings', array( $compatibility, 'set_product_breadcrumbs_preferred_taxonomy' ), 10 );
+			remove_filter( 'block_core_breadcrumbs_items', array( $compatibility, 'apply_woocommerce_breadcrumb_filters' ), 10 );
+		}
+	}
+
+	/**
+	 * @testdox Should set product category as the preferred product breadcrumb taxonomy.
+	 */
+	public function test_sets_product_breadcrumbs_preferred_taxonomy(): void {
+		$category_id = $this->create_term( 'Shirts', 'product_cat', array( 'slug' => 'shirts' ) );
+		$product_id  = self::factory()->post->create(
+			array(
+				'post_type' => 'product',
+			)
+		);
+		wp_set_post_terms( $product_id, array( $category_id ), 'product_cat' );
+
+		$result = $this->sut->set_product_breadcrumbs_preferred_taxonomy( array(), 'product', $product_id );
+
+		$this->assertSame( 'product_cat', $result['taxonomy'], 'Products should prefer product categories.' );
+		$this->assertSame( 'shirts', $result['term'], 'Products should prefer WooCommerce-selected product category terms.' );
+	}
+
+	/**
+	 * @testdox Should apply the WooCommerce Home breadcrumb URL filter.
+	 */
+	public function test_core_breadcrumbs_apply_home_breadcrumb_url_filter(): void {
+		$callback = function () {
+			return home_url( '/storefront/' );
+		};
+		add_filter( 'woocommerce_breadcrumb_home_url', $callback );
+		$this->go_to( $this->get_product_archive_url() );
+		$this->set_product_archive_request();
+
+		try {
+			$result = $this->apply_core_breadcrumb_filters(
+				$this->get_breadcrumb_item( 'Current page' )
+			);
+		} finally {
+			remove_filter( 'woocommerce_breadcrumb_home_url', $callback );
+		}
+
+		$this->assertSame(
+			array(
+				$this->get_home_breadcrumb_item( home_url( '/storefront/' ) ),
+				$this->get_breadcrumb_item( 'Current page' ),
+			),
+			$result,
+			'Core Breadcrumbs should use the filtered WooCommerce Home breadcrumb URL.'
+		);
+	}
+
+	/**
+	 * @testdox Should apply the WooCommerce Home breadcrumb URL filter outside WooCommerce contexts.
+	 */
+	public function test_core_breadcrumbs_apply_home_breadcrumb_url_filter_outside_woocommerce_contexts(): void {
+		$callback = function () {
+			return home_url( '/storefront/' );
+		};
+		add_filter( 'woocommerce_breadcrumb_home_url', $callback );
+		$this->go_to( '/?s=breadcrumb' );
+
+		try {
+			$result = $this->apply_core_breadcrumb_filters(
+				$this->get_breadcrumb_item( 'Search results for: "breadcrumb"' )
+			);
+		} finally {
+			remove_filter( 'woocommerce_breadcrumb_home_url', $callback );
+		}
+
+		$this->assertSame(
+			home_url( '/storefront/' ),
+			$result[0]['url'],
+			'Core Breadcrumbs should use the filtered WooCommerce Home breadcrumb URL outside WooCommerce contexts.'
+		);
+	}
+
+	/**
+	 * @testdox Should use the default Home breadcrumb URL when the WooCommerce Home URL filter returns a non-string.
+	 */
+	public function test_core_breadcrumbs_use_default_home_breadcrumb_url_for_non_string_filter_value(): void {
+		$callback = function () {
+			return array();
+		};
+		add_filter( 'woocommerce_breadcrumb_home_url', $callback );
+		$this->go_to( $this->get_product_archive_url() );
+		$this->set_product_archive_request();
+
+		try {
+			$result = $this->apply_core_breadcrumb_filters(
+				$this->get_breadcrumb_item( 'Current page' )
+			);
+		} finally {
+			remove_filter( 'woocommerce_breadcrumb_home_url', $callback );
+		}
+
+		$this->assertSame(
+			home_url(),
+			$result[0]['url'],
+			'Core Breadcrumbs should fall back to the default WooCommerce Home breadcrumb URL.'
+		);
+	}
+
+	/**
+	 * @testdox Should use the shop page title for product archive breadcrumbs.
+	 */
+	public function test_core_breadcrumbs_use_shop_page_title_for_product_archive_item(): void {
+		$product_item = $this->get_breadcrumb_item( 'Logo Tee' );
+		$this->go_to( $this->get_product_archive_url() );
+		$this->set_product_archive_request();
+
+		$result = $this->apply_core_breadcrumb_filters(
+			$this->get_breadcrumb_item( 'All Products', $this->get_product_archive_url() ),
+			$product_item
+		);
+
+		$this->assertSame(
+			array( 'Home', 'Catalog', 'Logo Tee' ),
+			$this->get_breadcrumb_labels( $result ),
+			'Product archive breadcrumb should use the Shop page title.'
+		);
+		$this->assertSame(
+			$this->get_core_breadcrumb_items(
+				$this->get_breadcrumb_item( 'Catalog', $this->get_product_archive_url() ),
+				$product_item
+			),
+			$result,
+			'Product archive breadcrumb should preserve Core item shape while updating the label.'
+		);
+	}
+
+	/**
+	 * @testdox Should prepend the shop page to product category breadcrumbs.
+	 */
+	public function test_core_breadcrumbs_prepend_shop_page_to_product_category_items(): void {
+		$category_id = $this->create_term( 'Shirts', 'product_cat', array( 'slug' => 'shirts' ) );
+		$this->go_to( get_term_link( $category_id, 'product_cat' ) );
+
+		$this->assert_core_breadcrumb_labels(
+			array( 'Home', 'Catalog', 'Shirts' ),
+			'Product category breadcrumbs should include the Shop page crumb.',
+			$this->get_breadcrumb_item( 'Shirts' )
+		);
+	}
+
+	/**
+	 * @testdox Should not duplicate the shop page when Core already includes it.
+	 */
+	public function test_core_breadcrumbs_do_not_duplicate_existing_shop_page_item(): void {
+		$category_id = $this->create_term( 'Shirts', 'product_cat', array( 'slug' => 'shirts' ) );
+		$this->go_to( get_term_link( $category_id, 'product_cat' ) );
+
+		$shop_url   = untrailingslashit( $this->get_shop_page_url() );
+		$shirt_item = $this->get_breadcrumb_item( 'Shirts' );
+		$result     = $this->apply_core_breadcrumb_filters(
+			$this->get_breadcrumb_item( 'Existing catalog', $shop_url ),
+			$shirt_item
+		);
+
+		$this->assertSame(
+			$this->get_core_breadcrumb_items(
+				$this->get_breadcrumb_item( 'Catalog', $shop_url ),
+				$shirt_item
+			),
+			$result,
+			'Product category breadcrumbs should reuse the existing Shop crumb instead of inserting another one.'
+		);
+	}
+
+	/**
+	 * @testdox Should not prepend the shop page when product permalinks do not include the shop slug.
+	 */
+	public function test_core_breadcrumbs_do_not_prepend_shop_page_for_product_base_without_shop_slug(): void {
+		$woocommerce_permalinks                 = is_array( $this->original_woocommerce_permalinks ) ? $this->original_woocommerce_permalinks : array();
+		$woocommerce_permalinks['product_base'] = '/product';
+		update_option( 'woocommerce_permalinks', $woocommerce_permalinks );
+
+		$category_id = $this->create_term( 'Shirts', 'product_cat', array( 'slug' => 'shirts' ) );
+		$this->go_to( get_term_link( $category_id, 'product_cat' ) );
+
+		$this->assert_core_breadcrumbs_unchanged(
+			'Product category breadcrumbs should remain unchanged when WooCommerce would not prepend Shop.',
+			$this->get_breadcrumb_item( 'Shirts' )
+		);
+	}
+
+	/**
+	 * @testdox Should preserve pagination when labeling product tag breadcrumbs.
+	 */
+	public function test_core_breadcrumbs_label_paginated_product_tag_items(): void {
+		$tag_id = $this->create_term( 'Sale', 'product_tag', array( 'slug' => 'sale' ) );
+		$this->go_to( get_term_link( $tag_id, 'product_tag' ) );
+		set_query_var( 'paged', 2 );
+
+		$this->assert_core_breadcrumb_labels(
+			array( 'Home', 'Catalog', 'Products tagged &ldquo;Sale&rdquo;', 'Page 2' ),
+			'Paginated product tag breadcrumbs should keep the pagination crumb.',
+			$this->get_breadcrumb_item( 'Sale', get_term_link( $tag_id, 'product_tag' ) ),
+			$this->get_breadcrumb_item( 'Page 2' )
+		);
+	}
+
+	/**
+	 * @testdox Should preserve pagination when labeling product search breadcrumbs.
+	 */
+	public function test_core_breadcrumbs_label_paginated_product_search_items(): void {
+		global $wp_query;
+
+		$this->go_to( '/?s=hoodie&post_type=product' );
+
+		$wp_query->is_search            = true;
+		$wp_query->is_post_type_archive = true;
+		$wp_query->is_archive           = true;
+		$wp_query->is_404               = false;
+
+		set_query_var( 'paged', 2 );
+
+		$this->assert_core_breadcrumb_labels(
+			array( 'Home', 'Catalog', 'Search results for &ldquo;hoodie&rdquo;', 'Page 2' ),
+			'Paginated product search breadcrumbs should keep the pagination crumb.',
+			$this->get_breadcrumb_item( 'Search results for: "hoodie"', get_pagenum_link( 1 ) ),
+			$this->get_breadcrumb_item( 'Page 2' )
+		);
+	}
+
+	/**
+	 * @testdox Should preserve the Shop breadcrumb when the current product search item is hidden.
+	 */
+	public function test_core_breadcrumbs_preserve_shop_item_when_product_search_current_item_is_hidden(): void {
+		global $wp_query;
+
+		$this->go_to( '/?s=hoodie&post_type=product' );
+
+		$wp_query->is_search            = true;
+		$wp_query->is_post_type_archive = true;
+		$wp_query->is_archive           = true;
+		$wp_query->is_404               = false;
+
+		$result = $this->apply_core_breadcrumb_filters();
+
+		$this->assertSame(
+			array( 'Home', 'Catalog' ),
+			$this->get_breadcrumb_labels( $result ),
+			'The Shop crumb should not be relabeled as the current search when Core omits that item.'
+		);
+	}
+
+	/**
+	 * @testdox Should preserve Core's hidden Home setting on product searches.
+	 */
+	public function test_core_breadcrumbs_preserve_hidden_home_item_on_product_search(): void {
+		global $wp_query;
+
+		$this->go_to( '/?s=hoodie&post_type=product' );
+
+		$wp_query->is_search            = true;
+		$wp_query->is_post_type_archive = true;
+		$wp_query->is_archive           = true;
+		$wp_query->is_404               = false;
+
+		$result = $this->apply_breadcrumb_filters(
+			array( $this->get_breadcrumb_item( 'Search results for: "hoodie"' ) )
+		);
+
+		$this->assertSame(
+			array( 'Catalog', 'Search results for &ldquo;hoodie&rdquo;' ),
+			$this->get_breadcrumb_labels( $result ),
+			'The Home crumb should remain hidden when WooCommerce compatibility adds the Shop crumb.'
+		);
+	}
+
+	/**
+	 * @testdox Should preserve Core's hidden Home and current item settings on product searches.
+	 */
+	public function test_core_breadcrumbs_preserve_hidden_home_and_current_items_on_product_search(): void {
+		global $wp_query;
+
+		$this->go_to( '/?s=hoodie&post_type=product' );
+
+		$wp_query->is_search            = true;
+		$wp_query->is_post_type_archive = true;
+		$wp_query->is_archive           = true;
+		$wp_query->is_404               = false;
+
+		$result = $this->apply_breadcrumb_filters( array() );
+
+		$this->assertSame(
+			array( 'Catalog' ),
+			$this->get_breadcrumb_labels( $result ),
+			'Home and current crumbs should remain hidden while WooCommerce compatibility keeps the Shop parent.'
+		);
+	}
+
+	/**
+	 * @testdox Should use WooCommerce labels for regular search breadcrumbs.
+	 */
+	public function test_core_breadcrumbs_label_regular_search_items(): void {
+		$this->go_to( '/?s=breadcrumb' );
+
+		$this->assert_core_breadcrumb_labels(
+			array( 'Home', 'Search results for &ldquo;breadcrumb&rdquo;' ),
+			'Regular search breadcrumbs should use the WooCommerce search label.',
+			$this->get_breadcrumb_item( 'Search results for: "breadcrumb"' )
+		);
+	}
+
+	/**
+	 * @testdox Should prepend taxonomy labels for product attribute breadcrumbs.
+	 */
+	public function test_core_breadcrumbs_prepend_taxonomy_label_to_product_attribute_items(): void {
+		register_taxonomy(
+			'pa_test_color',
+			array( 'product' ),
+			array(
+				'labels'       => array(
+					'name'          => 'Colors',
+					'singular_name' => 'Color',
+				),
+				'public'       => true,
+				'show_in_rest' => true,
+				'hierarchical' => true,
+			)
+		);
+
+		$term_id = $this->create_term( 'Blue', 'pa_test_color', array( 'slug' => 'blue' ) );
+		$this->go_to( get_term_link( $term_id, 'pa_test_color' ) );
+		$this->set_product_taxonomy_request( $term_id, 'pa_test_color' );
+
+		$this->assert_core_breadcrumb_labels(
+			array( 'Home', 'Colors', 'Blue' ),
+			'Product attribute breadcrumbs should include the attribute taxonomy label crumb.',
+			$this->get_breadcrumb_item( 'Blue' )
+		);
+	}
+
+	/**
+	 * @testdox Should prepend the My Account page to endpoint breadcrumbs.
+	 */
+	public function test_core_breadcrumbs_prepend_my_account_page_to_endpoint_items(): void {
+		$this->set_account_endpoint_request( 'orders' );
+
+		$this->assert_core_breadcrumb_labels(
+			array( 'Home', 'My account', 'Orders' ),
+			'My Account endpoint breadcrumbs should include the account page crumb.',
+			$this->get_breadcrumb_item( 'Orders' )
+		);
+	}
+
+	/**
+	 * @testdox Should replace the account page title with the current endpoint title.
+	 */
+	public function test_core_breadcrumbs_replace_account_page_title_with_endpoint_title(): void {
+		$this->set_account_endpoint_request( 'orders' );
+
+		$this->assert_core_breadcrumb_labels(
+			array( 'Home', 'My account', 'Orders' ),
+			'My Account endpoint breadcrumbs should use the endpoint title for the current item.',
+			$this->get_breadcrumb_item( 'My account' )
+		);
+	}
+
+	/**
+	 * @testdox Should not duplicate the My Account page on endpoint breadcrumbs.
+	 */
+	public function test_core_breadcrumbs_do_not_duplicate_existing_my_account_page_item(): void {
+		$this->set_account_endpoint_request( 'orders' );
+
+		$this->assert_core_breadcrumbs_unchanged(
+			'My Account endpoint breadcrumbs should reuse the existing My Account crumb.',
+			$this->get_breadcrumb_item( 'My account', get_permalink( $this->my_account_page_id ) ),
+			$this->get_breadcrumb_item( 'Orders' )
+		);
+	}
+
+	/**
+	 * @testdox Should run the legacy WooCommerce breadcrumb filter after adjustments.
+	 */
+	public function test_core_breadcrumbs_apply_legacy_woocommerce_get_breadcrumb_filter_after_adjustments(): void {
+		$callback = function ( $crumbs ) {
+			$crumbs[1][0] = 'Filtered Catalog';
+			return $crumbs;
+		};
+		add_filter( 'woocommerce_get_breadcrumb', $callback );
+		$this->go_to( $this->get_product_archive_url() );
+		$this->set_product_archive_request();
+
+		try {
+			$result = $this->apply_core_breadcrumb_filters(
+				$this->get_breadcrumb_item( 'All Products', $this->get_product_archive_url() ),
+				$this->get_breadcrumb_item( 'Logo Tee' )
+			);
+		} finally {
+			remove_filter( 'woocommerce_get_breadcrumb', $callback );
+		}
+
+		$this->assertSame(
+			array( 'Home', 'Filtered Catalog', 'Logo Tee' ),
+			$this->get_breadcrumb_labels( $result ),
+			'Legacy breadcrumb filters should receive WooCommerce-adjusted Core breadcrumb items.'
+		);
+	}
+
+	/**
+	 * @testdox Should apply legacy WooCommerce breadcrumb filters outside WooCommerce contexts.
+	 */
+	public function test_core_breadcrumbs_apply_legacy_woocommerce_get_breadcrumb_filter_outside_woocommerce_contexts(): void {
+		$callback = function ( $crumbs ) {
+			$crumbs[1][0] = 'Filtered page';
+			return $crumbs;
+		};
+		add_filter( 'woocommerce_get_breadcrumb', $callback );
+		$this->go_to( '/?s=breadcrumb' );
+
+		try {
+			$result = $this->apply_core_breadcrumb_filters(
+				$this->get_breadcrumb_item( 'Search results for: "breadcrumb"' )
+			);
+		} finally {
+			remove_filter( 'woocommerce_get_breadcrumb', $callback );
+		}
+
+		$this->assertSame(
+			array( 'Home', 'Filtered page' ),
+			$this->get_breadcrumb_labels( $result ),
+			'Legacy WooCommerce breadcrumb filters should run for regular Core breadcrumbs.'
+		);
+	}
+
+	/**
+	 * @testdox Should preserve Core breadcrumb item metadata when legacy filters run.
+	 */
+	public function test_core_breadcrumbs_preserve_core_item_metadata_when_legacy_filters_run(): void {
+		$callback = function ( $crumbs ) {
+			return $crumbs;
+		};
+		add_filter( 'woocommerce_get_breadcrumb', $callback );
+		$this->go_to( $this->get_product_archive_url() );
+		$this->set_product_archive_request();
+
+		$product_item = $this->get_breadcrumb_item(
+			'<span>Logo Tee</span>',
+			null,
+			array(
+				'allow_html' => true,
+			)
+		);
+
+		try {
+			$result = $this->apply_core_breadcrumb_filters( $product_item );
+		} finally {
+			remove_filter( 'woocommerce_get_breadcrumb', $callback );
+		}
+
+		$this->assertSame(
+			$this->get_core_breadcrumb_items( $product_item ),
+			$result,
+			'Core breadcrumb item metadata should survive WooCommerce breadcrumb filter conversion.'
+		);
+	}
+
+	/**
+	 * Get the Home breadcrumb item.
+	 *
+	 * @param string|null $url Home URL.
+	 * @return array Home breadcrumb item.
+	 */
+	private function get_home_breadcrumb_item( ?string $url = null ): array {
+		return $this->get_breadcrumb_item( 'Home', $url ?? home_url( '/' ) );
+	}
+
+	/**
+	 * Get a breadcrumb item.
+	 *
+	 * @param string      $label Breadcrumb label.
+	 * @param string|null $url Breadcrumb URL.
+	 * @param array       $metadata Extra breadcrumb metadata.
+	 * @return array Breadcrumb item.
+	 */
+	private function get_breadcrumb_item( string $label, ?string $url = null, array $metadata = array() ): array {
+		$item = array_merge(
+			array(
+				'label' => $label,
+			),
+			$metadata
+		);
+
+		if ( null !== $url ) {
+			$item['url'] = $url;
+		}
+
+		return $item;
+	}
+
+	/**
+	 * Get Core breadcrumb items with a Home item.
+	 *
+	 * @param array ...$items Breadcrumb items after Home.
+	 * @return array Breadcrumb items.
+	 */
+	private function get_core_breadcrumb_items( array ...$items ): array {
+		array_unshift( $items, $this->get_home_breadcrumb_item() );
+
+		return $items;
+	}
+
+	/**
+	 * Apply WooCommerce breadcrumb compatibility filters to Core breadcrumb items.
+	 *
+	 * @param array ...$items Breadcrumb items after Home.
+	 * @return array Filtered breadcrumb items.
+	 */
+	private function apply_core_breadcrumb_filters( array ...$items ): array {
+		return $this->apply_breadcrumb_filters( $this->get_core_breadcrumb_items( ...$items ) );
+	}
+
+	/**
+	 * Apply WooCommerce breadcrumb compatibility filters.
+	 *
+	 * @param array $items Breadcrumb items.
+	 * @return array Filtered breadcrumb items.
+	 */
+	private function apply_breadcrumb_filters( array $items ): array {
+		return $this->sut->apply_woocommerce_breadcrumb_filters( $items );
+	}
+
+	/**
+	 * Assert breadcrumb labels after applying filters to Core breadcrumb items.
+	 *
+	 * @param array  $expected_labels Expected breadcrumb labels.
+	 * @param string $message Assertion message.
+	 * @param array  ...$items Breadcrumb items after Home.
+	 */
+	private function assert_core_breadcrumb_labels( array $expected_labels, string $message, array ...$items ): void {
+		$this->assertSame(
+			$expected_labels,
+			$this->get_breadcrumb_labels( $this->apply_core_breadcrumb_filters( ...$items ) ),
+			$message
+		);
+	}
+
+	/**
+	 * Assert Core breadcrumb items are unchanged after applying filters.
+	 *
+	 * @param string $message Assertion message.
+	 * @param array  ...$items Breadcrumb items after Home.
+	 */
+	private function assert_core_breadcrumbs_unchanged( string $message, array ...$items ): void {
+		$core_items = $this->get_core_breadcrumb_items( ...$items );
+
+		$this->assertSame(
+			$core_items,
+			$this->apply_breadcrumb_filters( $core_items ),
+			$message
+		);
+	}
+
+	/**
+	 * Get labels from breadcrumb items.
+	 *
+	 * @param array $items Breadcrumb items.
+	 * @return array Breadcrumb labels.
+	 */
+	private function get_breadcrumb_labels( array $items ): array {
+		return array_column( $items, 'label' );
+	}
+
+	/**
+	 * Get the product archive URL.
+	 *
+	 * @return string Product archive URL.
+	 */
+	private function get_product_archive_url(): string {
+		$url = get_post_type_archive_link( 'product' );
+
+		if ( ! is_string( $url ) ) {
+			$this->fail( 'Expected the product archive link to be available.' );
+		}
+
+		return $url;
+	}
+
+	/**
+	 * Get the Shop page URL.
+	 *
+	 * @return string Shop page URL.
+	 */
+	private function get_shop_page_url(): string {
+		$url = get_permalink( $this->shop_page_id );
+
+		if ( ! is_string( $url ) ) {
+			$this->fail( 'Expected the Shop page permalink to be available.' );
+		}
+
+		return $url;
+	}
+
+	/**
+	 * Set the current request as a My Account endpoint.
+	 *
+	 * @param string $endpoint Endpoint name.
+	 */
+	private function set_account_endpoint_request( string $endpoint ): void {
+		global $wp, $wp_query;
+
+		WC()->query = new \WC_Query();
+
+		if ( ! $wp instanceof \WP ) {
+			$wp = new \WP(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		}
+
+		if ( ! $wp_query instanceof \WP_Query ) {
+			$wp_query = new \WP_Query(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		}
+
+		if ( ! is_array( $wp->query_vars ) ) {
+			$wp->query_vars = array();
+		}
+
+		$wp->query_vars[ $endpoint ] = '';
+
+		add_filter( 'woocommerce_is_account_page', '__return_true' );
+	}
+
+	/**
+	 * Set the current request as the product archive.
+	 */
+	private function set_product_archive_request(): void {
+		global $wp_query;
+
+		$wp_query->is_post_type_archive = true;
+		$wp_query->is_archive           = true;
+		$wp_query->is_404               = false;
+
+		set_query_var( 'post_type', 'product' );
+	}
+
+	/**
+	 * Set the current request as a product taxonomy archive.
+	 *
+	 * @param int    $term_id Term ID.
+	 * @param string $taxonomy Taxonomy name.
+	 */
+	private function set_product_taxonomy_request( int $term_id, string $taxonomy ): void {
+		global $wp_query;
+
+		$term = get_term( $term_id, $taxonomy );
+
+		if ( ! $term instanceof \WP_Term ) {
+			$this->fail( 'Expected product taxonomy term to be available.' );
+		}
+
+		$wp_query->queried_object    = $term;
+		$wp_query->queried_object_id = $term_id;
+		$wp_query->is_tax            = true;
+		$wp_query->is_archive        = true;
+		$wp_query->is_404            = false;
+
+		set_query_var( 'taxonomy', $taxonomy );
+		set_query_var( 'term', $term->slug );
+	}
+
+	/**
+	 * Create a taxonomy term.
+	 *
+	 * @param string $name Term name.
+	 * @param string $taxonomy Taxonomy name.
+	 * @param array  $args Term arguments.
+	 * @return int Term ID.
+	 */
+	private function create_term( string $name, string $taxonomy, array $args = array() ): int {
+		$term = wp_insert_term( $name, $taxonomy, $args );
+
+		if ( is_wp_error( $term ) ) {
+			$this->fail( $term->get_error_message() );
+		}
+
+		return (int) $term['term_id'];
+	}
+}