From 4a92c0559bd357a63e4951c794f7e5b306aa6988 Mon Sep 17 00:00:00 2001 From: Mustafa Bharmal Date: Tue, 19 May 2026 15:56:15 +0530 Subject: [PATCH 01/12] Block Supports: Prevent Additional CSS duplication inside Query Loop --- src/wp-includes/block-supports/custom-css.php | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index c947609a35698..7b1fdf03c8669 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -57,12 +57,22 @@ function wp_render_custom_css_support_styles( $parsed_block ) { if ( ! empty( $processed_css ) ) { /* - * Register and add inline style for block custom CSS. - * The style depends on global-styles to ensure custom CSS loads after - * and can override global styles. - */ - wp_register_style( 'wp-block-custom-css', false, array( 'global-styles' ) ); - wp_add_inline_style( 'wp-block-custom-css', $processed_css ); + * Track which class names have already had their CSS enqueued to prevent + * duplicate styles when the same block is rendered multiple times inside + * a Query Loop (render_block_data fires once per loop iteration). + */ + static $enqueued_class_names = array(); + + if ( ! isset( $enqueued_class_names[ $class_name ] ) ) { + $enqueued_class_names[ $class_name ] = true; + /* + * Register and add inline style for block custom CSS. + * The style depends on global-styles to ensure custom CSS loads after + * and can override global styles. + */ + wp_register_style( 'wp-block-custom-css', false, array( 'global-styles' ) ); + wp_add_inline_style( 'wp-block-custom-css', $processed_css ); + } } return $parsed_block; From 1d6d46cc2fe919e93bde40b9e37f12396d19260e Mon Sep 17 00:00:00 2001 From: Mustafa Bharmal Date: Tue, 19 May 2026 17:18:37 +0530 Subject: [PATCH 02/12] Reformat block comment indentation in custom-css.php --- src/wp-includes/block-supports/custom-css.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index 7b1fdf03c8669..06cd5404ec2ee 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -57,19 +57,19 @@ function wp_render_custom_css_support_styles( $parsed_block ) { if ( ! empty( $processed_css ) ) { /* - * Track which class names have already had their CSS enqueued to prevent - * duplicate styles when the same block is rendered multiple times inside - * a Query Loop (render_block_data fires once per loop iteration). - */ + * Track which class names have already had their CSS enqueued to prevent + * duplicate styles when the same block is rendered multiple times inside + * a Query Loop (render_block_data fires once per loop iteration). + */ static $enqueued_class_names = array(); if ( ! isset( $enqueued_class_names[ $class_name ] ) ) { $enqueued_class_names[ $class_name ] = true; /* - * Register and add inline style for block custom CSS. - * The style depends on global-styles to ensure custom CSS loads after - * and can override global styles. - */ + * Register and add inline style for block custom CSS. + * The style depends on global-styles to ensure custom CSS loads after + * and can override global styles. + */ wp_register_style( 'wp-block-custom-css', false, array( 'global-styles' ) ); wp_add_inline_style( 'wp-block-custom-css', $processed_css ); } From f7b4679cdd9783ad12295259c1af25feb1859fe3 Mon Sep 17 00:00:00 2001 From: Mustafa Bharmal Date: Wed, 3 Jun 2026 18:15:52 +0530 Subject: [PATCH 03/12] Refactor: custom CSS handling to prevent duplicate styles in Query Loop --- src/wp-includes/block-supports/custom-css.php | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index 06cd5404ec2ee..4ff5f7cab2dd2 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -61,17 +61,16 @@ function wp_render_custom_css_support_styles( $parsed_block ) { * duplicate styles when the same block is rendered multiple times inside * a Query Loop (render_block_data fires once per loop iteration). */ - static $enqueued_class_names = array(); - - if ( ! isset( $enqueued_class_names[ $class_name ] ) ) { - $enqueued_class_names[ $class_name ] = true; - /* - * Register and add inline style for block custom CSS. - * The style depends on global-styles to ensure custom CSS loads after - * and can override global styles. - */ - wp_register_style( 'wp-block-custom-css', false, array( 'global-styles' ) ); - wp_add_inline_style( 'wp-block-custom-css', $processed_css ); + $handle = 'wp-block-custom-css'; + if ( ! wp_style_is( $handle, 'registered' ) ) { + wp_register_style( $handle, false, array( 'global-styles' ) ); + } + $after_styles = wp_styles()->get_data( $handle, 'after' ); + if ( ! is_array( $after_styles ) ) { + $after_styles = array(); + } + if ( ! in_array( $processed_css, $after_styles, true ) ) { + wp_add_inline_style( $handle, $processed_css ); } } From fd013775914545e6bd20ce5f62ad48bfbc1f030b Mon Sep 17 00:00:00 2001 From: Mustafa Bharmal Date: Wed, 1 Jul 2026 00:35:00 +0530 Subject: [PATCH 04/12] add: testcase for the issue --- .../wpRenderCustomCssSupportStyles.php | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php index 0048b379f8f33..99b32ecb4aa79 100644 --- a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php +++ b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php @@ -264,4 +264,63 @@ public function data_does_not_add_class_name() { ), ); } + + /** + * Tests that CSS is enqueued only once when the same block is rendered + * multiple times, as happens inside a Query Loop. + * + * @ticket 65268 + * + * @covers ::wp_render_custom_css_support_styles + */ + public function test_css_not_duplicated_on_repeated_renders() { + $this->test_block_name = 'test/custom-css-query-loop-dedup'; + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( 'customCSS' => true ), + ) + ); + + $parsed_block = array( + 'blockName' => 'test/custom-css-query-loop-dedup', + 'attrs' => array( + 'style' => array( + 'css' => 'font-size: 2em; /* query-loop-dedup-test */', + ), + ), + ); + + // Simulate the same block being rendered multiple times inside a Query Loop. + $result = wp_render_custom_css_support_styles( $parsed_block ); + wp_render_custom_css_support_styles( $parsed_block ); + wp_render_custom_css_support_styles( $parsed_block ); + + // Extract the generated class name from the first render's result. + preg_match( '/wp-custom-css-\S+/', $result['attrs']['className'], $matches ); + $class_name = $matches[0]; + + // Count how many times the CSS selector for this block appears in the enqueued inline styles. + $inline_styles = (array) wp_styles()->get_data( 'wp-block-custom-css', 'after' ); + $occurrences = 0; + foreach ( $inline_styles as $style ) { + if ( false !== strpos( $style, '.' . $class_name ) ) { + ++$occurrences; + } + } + + $this->assertSame( + 1, + $occurrences, + 'CSS should be enqueued exactly once even when the same block renders multiple times.' + ); + + wp_deregister_style( 'wp-block-custom-css' ); + } } From 25ebd8c9bb21db3ecf190e26f3bed00f97aa1400 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Jul 2026 17:41:14 -0700 Subject: [PATCH 05/12] Update comment Co-authored-by: t-hamano --- src/wp-includes/block-supports/custom-css.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index 4ff5f7cab2dd2..3e7546d14630a 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -56,10 +56,11 @@ function wp_render_custom_css_support_styles( $parsed_block ) { $processed_css = WP_Theme_JSON::process_blocks_custom_css( $custom_css, $selector ); if ( ! empty( $processed_css ) ) { - /* - * Track which class names have already had their CSS enqueued to prevent - * duplicate styles when the same block is rendered multiple times inside - * a Query Loop (render_block_data fires once per loop iteration). + /** + * Skip CSS that has already been added. Blocks with identical attributes + * share the same class name and processed CSS via {@see wp_unique_id_from_values()}, + * so the same style would otherwise be enqueued more than once (e.g. inside + * a Query Loop or when blocks share identical custom CSS). */ $handle = 'wp-block-custom-css'; if ( ! wp_style_is( $handle, 'registered' ) ) { From ef001a3f7c1b2973f63ee320452e6ed30a869c0d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Jul 2026 17:46:31 -0700 Subject: [PATCH 06/12] Add return type for wp_render_custom_css_support_styles() --- src/wp-includes/block-supports/custom-css.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index 3e7546d14630a..653b6a2f15c5b 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -25,6 +25,18 @@ * }, * ... * } $parsed_block + * @phpstan-return array{ + * blockName: string|null, + * attrs: array{ + * className?: string, + * style?: array{ + * css?: string, + * ... + * }, + * ... + * }, + * ... + * } */ function wp_render_custom_css_support_styles( $parsed_block ) { $custom_css = $parsed_block['attrs']['style']['css'] ?? null; From c78ddac9b881c2dc2e1358454b83b5e986306534 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Jul 2026 17:50:06 -0700 Subject: [PATCH 07/12] Refine test logic --- .../block-supports/wpRenderCustomCssSupportStyles.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php index 99b32ecb4aa79..1646adc1fa1bd 100644 --- a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php +++ b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php @@ -273,7 +273,7 @@ public function data_does_not_add_class_name() { * * @covers ::wp_render_custom_css_support_styles */ - public function test_css_not_duplicated_on_repeated_renders() { + public function test_css_not_duplicated_on_repeated_renders(): void { $this->test_block_name = 'test/custom-css-query-loop-dedup'; register_block_type( $this->test_block_name, @@ -303,16 +303,15 @@ public function test_css_not_duplicated_on_repeated_renders() { wp_render_custom_css_support_styles( $parsed_block ); // Extract the generated class name from the first render's result. - preg_match( '/wp-custom-css-\S+/', $result['attrs']['className'], $matches ); - $class_name = $matches[0]; + $this->assertSame( 1, preg_match( '/(^|\s)(wp-custom-css-\S+)/', $result['attrs']['className'] ?? '', $matches ) ); + $class_name = $matches[1]; // Count how many times the CSS selector for this block appears in the enqueued inline styles. $inline_styles = (array) wp_styles()->get_data( 'wp-block-custom-css', 'after' ); $occurrences = 0; foreach ( $inline_styles as $style ) { - if ( false !== strpos( $style, '.' . $class_name ) ) { - ++$occurrences; - } + $this->assertIsString( $style ); + $occurrences += substr_count( $style, '.' . $class_name ); } $this->assertSame( From 9d785fea60a422b2031da3c2b8010650ebd097ae Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Jul 2026 17:53:01 -0700 Subject: [PATCH 08/12] Unset styles resistry at set_up/tear_down instead of deregistering style at end of test --- .../block-supports/wpRenderCustomCssSupportStyles.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php index 1646adc1fa1bd..2a1e1ed1a8475 100644 --- a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php +++ b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php @@ -14,6 +14,9 @@ class Tests_Block_Supports_WpRenderCustomCssSupportStyles extends WP_UnitTestCas public function set_up() { parent::set_up(); $this->test_block_name = null; + + global $wp_styles; + $wp_styles = null; } public function tear_down() { @@ -21,6 +24,10 @@ public function tear_down() { unregister_block_type( $this->test_block_name ); } $this->test_block_name = null; + + global $wp_styles; + $wp_styles = null; + parent::tear_down(); } @@ -319,7 +326,5 @@ public function test_css_not_duplicated_on_repeated_renders(): void { $occurrences, 'CSS should be enqueued exactly once even when the same block renders multiple times.' ); - - wp_deregister_style( 'wp-block-custom-css' ); } } From 68a7ac7b9bdcbe3d43b5b8e2b217f6c585fe29d6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Jul 2026 18:04:00 -0700 Subject: [PATCH 09/12] Eliminate delimiter from being in capture group --- .../tests/block-supports/wpRenderCustomCssSupportStyles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php index 2a1e1ed1a8475..e9556254311b8 100644 --- a/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php +++ b/tests/phpunit/tests/block-supports/wpRenderCustomCssSupportStyles.php @@ -310,7 +310,7 @@ public function test_css_not_duplicated_on_repeated_renders(): void { wp_render_custom_css_support_styles( $parsed_block ); // Extract the generated class name from the first render's result. - $this->assertSame( 1, preg_match( '/(^|\s)(wp-custom-css-\S+)/', $result['attrs']['className'] ?? '', $matches ) ); + $this->assertSame( 1, preg_match( '/(?:^|\s)(wp-custom-css-\S+)/', $result['attrs']['className'] ?? '', $matches ) ); $class_name = $matches[1]; // Count how many times the CSS selector for this block appears in the enqueued inline styles. From aa8abc452361844da33dcee6a419b0be0d5e352d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Jul 2026 18:23:58 -0700 Subject: [PATCH 10/12] Eliminate unnecessary `_wp_array_set()` in favor of simple array assignment In addition to being simpler, this ensures PHPStan is able to statically-analyze the return value. --- src/wp-includes/block-supports/custom-css.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index 653b6a2f15c5b..e952f2f22d88a 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -61,7 +61,7 @@ function wp_render_custom_css_support_styles( $parsed_block ) { ? "$existing_class_name $class_name" : $class_name; - _wp_array_set( $parsed_block, array( 'attrs', 'className' ), $updated_class_name ); + $parsed_block['attrs']['className'] = $updated_class_name; // Process the custom CSS using the same method as global styles. $selector = '.' . $class_name; From d151e2f238cafc186e49d7bd97641ca97849bcbf Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:07:40 +0900 Subject: [PATCH 11/12] Fix PHPDoc comment format Co-authored-by: Mukesh Panchal --- src/wp-includes/block-supports/custom-css.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index e952f2f22d88a..5008d679170fb 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -68,7 +68,7 @@ function wp_render_custom_css_support_styles( $parsed_block ) { $processed_css = WP_Theme_JSON::process_blocks_custom_css( $custom_css, $selector ); if ( ! empty( $processed_css ) ) { - /** + /* * Skip CSS that has already been added. Blocks with identical attributes * share the same class name and processed CSS via {@see wp_unique_id_from_values()}, * so the same style would otherwise be enqueued more than once (e.g. inside From a081dfda22e1256faf00dcb11489f55a910018ba Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 6 Jul 2026 17:15:38 +0900 Subject: [PATCH 12/12] Revert to the previous Dockblock style --- src/wp-includes/block-supports/custom-css.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/block-supports/custom-css.php b/src/wp-includes/block-supports/custom-css.php index 5008d679170fb..e952f2f22d88a 100644 --- a/src/wp-includes/block-supports/custom-css.php +++ b/src/wp-includes/block-supports/custom-css.php @@ -68,7 +68,7 @@ function wp_render_custom_css_support_styles( $parsed_block ) { $processed_css = WP_Theme_JSON::process_blocks_custom_css( $custom_css, $selector ); if ( ! empty( $processed_css ) ) { - /* + /** * Skip CSS that has already been added. Blocks with identical attributes * share the same class name and processed CSS via {@see wp_unique_id_from_values()}, * so the same style would otherwise be enqueued more than once (e.g. inside