diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php
index a8ed76d3bd71f..dff2ada10b6eb 100644
--- a/src/wp-includes/blocks.php
+++ b/src/wp-includes/blocks.php
@@ -1844,6 +1844,7 @@ function traverse_and_serialize_block( $block, $pre_callback = null, $post_callb
* Replaces patterns in a block tree with their content.
*
* @since 6.6.0
+ * @since 7.0.0 Adds metadata to attributes of single-pattern container blocks.
*
* @param array $blocks An array blocks.
*
@@ -1880,7 +1881,39 @@ function resolve_pattern_blocks( $blocks ) {
continue;
}
- $blocks_to_insert = parse_blocks( $pattern['content'] );
+ $blocks_to_insert = parse_blocks( trim( $pattern['content'] ) );
+
+ /*
+ * For single-root patterns, add the pattern name to make this a pattern instance in the editor.
+ * If the pattern has metadata, merge it with the existing metadata.
+ */
+ if ( count( $blocks_to_insert ) === 1 ) {
+ $block_metadata = $blocks_to_insert[0]['attrs']['metadata'] ?? array();
+ $block_metadata['patternName'] = $slug;
+
+ /*
+ * Merge pattern metadata with existing block metadata.
+ * Pattern metadata takes precedence, but existing block metadata
+ * is preserved as a fallback when the pattern doesn't define that field.
+ * Only the defined fields (name, description, categories) are updated;
+ * other metadata keys are preserved.
+ */
+ foreach ( array(
+ 'name' => 'title', // 'title' is the field in the pattern object 'name' is the field in the block metadata.
+ 'description' => 'description',
+ 'categories' => 'categories',
+ ) as $key => $pattern_key ) {
+ $value = $pattern[ $pattern_key ] ?? $block_metadata[ $key ] ?? null;
+ if ( $value ) {
+ $block_metadata[ $key ] = is_array( $value )
+ ? array_map( 'sanitize_text_field', $value )
+ : sanitize_text_field( $value );
+ }
+ }
+
+ $blocks_to_insert[0]['attrs']['metadata'] = $block_metadata;
+ }
+
$seen_refs[ $slug ] = true;
$prev_inner_content = $inner_content;
$inner_content = null;
diff --git a/tests/phpunit/tests/blocks/resolvePatternBlocks.php b/tests/phpunit/tests/blocks/resolvePatternBlocks.php
index b2e6fa6463f7d..4b28db4613f00 100644
--- a/tests/phpunit/tests/blocks/resolvePatternBlocks.php
+++ b/tests/phpunit/tests/blocks/resolvePatternBlocks.php
@@ -30,12 +30,74 @@ public function set_up() {
'description' => 'Recursive pattern.',
)
);
+ register_block_pattern(
+ 'core/single-root',
+ array(
+ 'title' => 'Single Root Pattern',
+ 'content' => 'Single root content',
+ 'description' => 'A single root pattern.',
+ 'categories' => array( 'text' ),
+ )
+ );
+ register_block_pattern(
+ 'core/single-root-with-forbidden-chars-in-attrs',
+ array(
+ 'title' => 'Single Root Pattern',
+ 'content' => 'Single root content',
+ 'description' => 'A single root pattern.
',
+ 'categories' => array(
+ 'text',
+ 'bad\'); DROP TABLE wp_posts;--',
+ '
',
+ "evil\x00null\nbyte",
+ 'category with html tags',
+ ),
+ )
+ );
+ register_block_pattern(
+ 'core/with-attrs',
+ array(
+ 'title' => 'Pattern With Attrs',
+ 'content' => 'Content',
+ 'description' => 'A pattern with existing attributes.',
+ )
+ );
+ register_block_pattern(
+ 'core/nested-single',
+ array(
+ 'title' => 'Nested Pattern',
+ 'content' => 'Nested content',
+ 'description' => 'A nested single root pattern.',
+ 'categories' => array( 'featured' ),
+ )
+ );
+ register_block_pattern(
+ 'core/existing-metadata',
+ array(
+ 'title' => 'Existing Metadata Pattern',
+ 'content' => 'Existing metadata content',
+ )
+ );
+ register_block_pattern(
+ 'core/with-custom-metadata',
+ array(
+ 'title' => 'Pattern With Custom Metadata',
+ 'content' => 'Content with custom metadata',
+ 'description' => 'A pattern with custom metadata keys.',
+ 'categories' => array( 'test' ),
+ )
+ );
}
public function tear_down() {
unregister_block_pattern( 'core/test' );
unregister_block_pattern( 'core/recursive' );
-
+ unregister_block_pattern( 'core/single-root' );
+ unregister_block_pattern( 'core/single-root-with-forbidden-chars-in-attrs' );
+ unregister_block_pattern( 'core/with-attrs' );
+ unregister_block_pattern( 'core/nested-single' );
+ unregister_block_pattern( 'core/existing-metadata' );
+ unregister_block_pattern( 'core/with-custom-metadata' );
parent::tear_down();
}
@@ -60,13 +122,55 @@ public function test_should_resolve_pattern_blocks_as_expected( $blocks, $expect
public function data_should_resolve_pattern_blocks_as_expected() {
return array(
// Works without attributes, leaves the block as is.
- 'pattern with no slug attribute' => array( '', '' ),
+ 'pattern with no slug attribute' => array(
+ '',
+ '',
+ ),
// Resolves the pattern.
- 'test pattern' => array( '', 'HelloWorld' ),
+ 'test pattern' => array(
+ '',
+ 'HelloWorld',
+ ),
// Skips recursive patterns.
- 'recursive pattern' => array( '', 'Recursive' ),
+ 'recursive pattern' => array(
+ '',
+ 'Recursive',
+ ),
// Resolves the pattern within a block.
- 'pattern within a block' => array( 'BeforeAfter', 'BeforeHelloWorldAfter' ),
+ 'pattern within a block' => array(
+ 'BeforeAfter',
+ 'BeforeHelloWorldAfter',
+ ),
+ // Resolves the single-root pattern and adds metadata.
+ 'single-root pattern' => array(
+ '',
+ 'Single root content',
+ ),
+ // Existing attributes are preserved when adding metadata.
+ 'existing attributes preserved' => array(
+ '',
+ 'Content',
+ ),
+ // Resolves the nested single-root pattern and adds metadata.
+ 'nested single-root pattern' => array(
+ '',
+ 'Nested contentSingle root content',
+ ),
+ // Sanitizes fields.
+ 'sanitized pattern attrs' => array(
+ '',
+ 'Single root content',
+ ),
+ // Metadata is merged with existing metadata and existing metadata is preserved.
+ 'existing metadata preserved' => array(
+ '',
+ 'Existing metadata content',
+ ),
+ // Custom metadata keys are preserved when resolving patterns.
+ 'custom metadata preserved' => array(
+ '',
+ 'Content with custom metadata',
+ ),
);
}
}