Commit e577679fd67 for woocommerce
commit e577679fd67d4d645d08fae767442f0db1d07e70
Author: Tung Du <dinhtungdu@gmail.com>
Date: Mon Jun 15 13:56:58 2026 +0700
Fix Product Collection context inside Query Loop (#65637)
* fix: scope product template context
* test: cover product template context scoping
* Add changefile(s) from automation for the following project(s): woocommerce
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/65637-wooplug-4673-product-collection-block-several-inner-blocks-dont-work b/plugins/woocommerce/changelog/65637-wooplug-4673-product-collection-block-several-inner-blocks-dont-work
new file mode 100644
index 00000000000..b707d3d86f3
--- /dev/null
+++ b/plugins/woocommerce/changelog/65637-wooplug-4673-product-collection-block-several-inner-blocks-dont-work
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix Product Collection inner blocks when rendered inside Query Loop post content.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
index f9867646ac9..354e7825c61 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductTemplate.php
@@ -93,28 +93,29 @@ class ProductTemplate extends AbstractBlock {
// Get an instance of the current Post Template block.
$block_instance = $block->parsed_block;
$product_id = (int) get_the_ID();
+ $post_type = get_post_type();
// Set the block name to one that does not correspond to an existing registered block.
// This ensures that for the inner instances of the Post Template block, we do not render any block supports.
$block_instance['blockName'] = 'core/null';
- // Relay the block context to the inner blocks.
- $available_context = array_merge(
- (array) $block->context,
- array(
- 'postType' => get_post_type(),
- 'postId' => $product_id,
- )
- );
+ $filter_block_context = static function ( $context ) use ( $product_id, $post_type ) {
+ $context['postType'] = $post_type;
+ $context['postId'] = $product_id;
+ return $context;
+ };
+ // Use an early priority so that other 'render_block_context' filters have access to the values.
+ add_filter( 'render_block_context', $filter_block_context, 1 );
// Render the inner blocks of the Post Template block with `dynamic` set to `false` to prevent calling
// `render_callback` and ensure that no wrapper markup is included.
$block_content = (
new WP_Block(
$block_instance,
- $available_context
+ $block->context
)
)->render( array( 'dynamic' => false ) );
+ remove_filter( 'render_block_context', $filter_block_context, 1 );
// Load product into the shared products store.
wc_interactivity_api_load_product(
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductTemplateTest.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductTemplateTest.php
new file mode 100644
index 00000000000..adf229a86e7
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductTemplateTest.php
@@ -0,0 +1,110 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Blocks\BlockTypes;
+
+use WC_Helper_Product;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the Product Template block type.
+ */
+class ProductTemplateTest extends WC_Unit_Test_Case {
+
+ /**
+ * Renders a Product Collection inside a Query Loop post content block.
+ *
+ * @param int $product_id Product ID.
+ * @param int $author_id Outer post author ID.
+ * @return string Rendered block markup.
+ */
+ private function render_product_collection_inside_query_loop( int $product_id, int $author_id ): string {
+ $product_collection = $this->get_product_collection_markup( $product_id );
+ $post_id = self::factory()->post->create(
+ array(
+ 'post_author' => $author_id,
+ 'post_title' => 'Post containing Product Collection',
+ 'post_content' => $product_collection,
+ )
+ );
+
+ $query_loop = sprintf(
+ '<!-- wp:query {"query":{"perPage":1,"postType":"post","order":"desc","orderBy":"date","author":"%1$d","search":"","exclude":[],"sticky":"","inherit":false}} -->
+<div class="wp-block-query"><!-- wp:post-template --><!-- wp:post-content /--><!-- /wp:post-template --></div>
+<!-- /wp:query -->',
+ $author_id
+ );
+
+ try {
+ return do_blocks( $query_loop );
+ } finally {
+ wp_delete_post( $post_id, true );
+ }
+ }
+
+ /**
+ * Gets Product Collection block markup for a hand-picked product.
+ *
+ * @param int $product_id Product ID.
+ * @return string Product Collection block markup.
+ */
+ private function get_product_collection_markup( int $product_id ): string {
+ $attributes = array(
+ 'queryId' => 0,
+ 'query' => array(
+ 'perPage' => 1,
+ 'pages' => 1,
+ 'offset' => 0,
+ 'postType' => 'product',
+ 'order' => 'asc',
+ 'orderBy' => 'post__in',
+ 'search' => '',
+ 'exclude' => array(),
+ 'inherit' => false,
+ 'taxQuery' => array(),
+ 'isProductCollectionBlock' => true,
+ 'featured' => false,
+ 'woocommerceOnSale' => false,
+ 'woocommerceStockStatus' => array( 'instock' ),
+ 'woocommerceAttributes' => array(),
+ 'woocommerceHandPickedProducts' => array( $product_id ),
+ 'filterable' => false,
+ ),
+ 'collection' => 'woocommerce/product-collection/hand-picked',
+ );
+
+ return sprintf(
+ '<!-- wp:woocommerce/product-collection %1$s -->
+<div class="wp-block-woocommerce-product-collection"><!-- wp:woocommerce/product-template -->
+<!-- wp:woocommerce/product-image /-->
+<!-- wp:woocommerce/product-price /-->
+<!-- /wp:woocommerce/product-template --></div>
+<!-- /wp:woocommerce/product-collection -->',
+ wp_json_encode( $attributes )
+ );
+ }
+
+ /**
+ * @testdox Should preserve product context when rendered inside a Query Loop post content block.
+ */
+ public function test_preserves_product_context_inside_query_loop_post_content(): void {
+ $product = WC_Helper_Product::create_simple_product(
+ true,
+ array(
+ 'regular_price' => 25,
+ 'price' => 25,
+ )
+ );
+ $author_id = self::factory()->user->create();
+
+ try {
+ $markup = $this->render_product_collection_inside_query_loop( $product->get_id(), $author_id );
+
+ $this->assertStringContainsString( 'wc-block-components-product-image', $markup, 'Product image should render using product context.' );
+ $this->assertStringContainsString( 'wc-block-components-product-price', $markup, 'Product price should render using product context.' );
+ } finally {
+ WC_Helper_Product::delete_product( $product->get_id() );
+ }
+ }
+}