Repository navigation
contentOnly patterns: mark patterns as contentOnly by adding metadata.patternName to the root block #72988
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ramonjd
merged 7 commits into
trunk
from
try/add-metadata-to-template-patterns-wpdev-10248
Nov 10, 2025
Merged
contentOnly patterns: mark patterns as contentOnly by adding metadata.patternName to the root block #72988
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
91f2b5a
Using get_block_template filters to resolve patterns. Added extra tes…
ramonjd fe5dcaf
Add backport changelog for PR #10248 and update test file formatting
ramonjd 6e8387a
Added comments for clarity on metadata merging logic and updated test…
ramonjd 718522a
Refactor metadata merging logic in pattern blocks to ensure existing …
ramonjd 821e21f
Update test case for existing metadata in pattern blocks to ensure co…
ramonjd 5c47fda
linty
ramonjd e76d2a4
Add custom metadata pattern block and update tests to verify metadata…
ramonjd File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| https://github.com/WordPress/wordpress-develop/pull/10248 | ||
|
|
||
| * https://github.com/WordPress/gutenberg/pull/72988 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| <?php | ||
| /** | ||
| * Compatibility shims for block APIs for WordPress 7.0. | ||
| * | ||
| * @package gutenberg | ||
| */ | ||
|
|
||
| if ( ! function_exists( 'gutenberg_resolve_pattern_blocks' ) ) { | ||
| /** | ||
| * 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. | ||
| * | ||
| * @return array An array of blocks with patterns replaced by their content. | ||
| */ | ||
| function gutenberg_resolve_pattern_blocks( $blocks ) { | ||
| static $inner_content; | ||
| // Keep track of seen references to avoid infinite loops. | ||
| static $seen_refs = array(); | ||
| $i = 0; | ||
| while ( $i < count( $blocks ) ) { | ||
| if ( 'core/pattern' === $blocks[ $i ]['blockName'] ) { | ||
| $attrs = $blocks[ $i ]['attrs']; | ||
|
|
||
| if ( empty( $attrs['slug'] ) ) { | ||
| ++$i; | ||
| continue; | ||
| } | ||
|
|
||
| $slug = $attrs['slug']; | ||
|
|
||
| if ( isset( $seen_refs[ $slug ] ) ) { | ||
| // Skip recursive patterns. | ||
| array_splice( $blocks, $i, 1 ); | ||
| continue; | ||
| } | ||
|
|
||
| $registry = WP_Block_Patterns_Registry::get_instance(); | ||
| $pattern = $registry->get_registered( $slug ); | ||
|
|
||
| // Skip unknown patterns. | ||
| if ( ! $pattern ) { | ||
| ++$i; | ||
| continue; | ||
| } | ||
| ////////////////////////////// | ||
| // START CORE MODIFICATIONS // | ||
| ////////////////////////////// | ||
| $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(); | ||
| $metadata = array( 'patternName' => $slug ); | ||
|
|
||
| foreach ( array( | ||
| 'name' => 'title', | ||
| 'description' => 'description', | ||
| 'categories' => 'categories', | ||
| ) as $key => $pattern_key ) { | ||
| $value = $pattern[ $pattern_key ] ?? $block_metadata[ $key ] ?? null; | ||
| if ( $value ) { | ||
| $metadata[ $key ] = 'categories' === $key && is_array( $value ) | ||
| ? array_map( 'sanitize_text_field', $value ) | ||
| : sanitize_text_field( $value ); | ||
| } | ||
| } | ||
|
|
||
| $blocks_to_insert[0]['attrs']['metadata'] = $metadata; | ||
|
ramonjd marked this conversation as resolved.
Resolved
|
||
| } | ||
| ////////////////////////////// | ||
| // END CORE MODIFICATIONS // | ||
| ////////////////////////////// | ||
|
|
||
| $seen_refs[ $slug ] = true; | ||
| $prev_inner_content = $inner_content; | ||
| $inner_content = null; | ||
| $blocks_to_insert = gutenberg_resolve_pattern_blocks( $blocks_to_insert ); | ||
| $inner_content = $prev_inner_content; | ||
| unset( $seen_refs[ $slug ] ); | ||
| array_splice( $blocks, $i, 1, $blocks_to_insert ); | ||
|
|
||
| // If we have inner content, we need to insert nulls in the | ||
| // inner content array, otherwise serialize_blocks will skip | ||
| // blocks. | ||
| if ( $inner_content ) { | ||
| $null_indices = array_keys( $inner_content, null, true ); | ||
| $content_index = $null_indices[ $i ]; | ||
| $nulls = array_fill( 0, count( $blocks_to_insert ), null ); | ||
| array_splice( $inner_content, $content_index, 1, $nulls ); | ||
| } | ||
|
|
||
| // Skip inserted blocks. | ||
| $i += count( $blocks_to_insert ); | ||
| } else { | ||
| if ( ! empty( $blocks[ $i ]['innerBlocks'] ) ) { | ||
| $prev_inner_content = $inner_content; | ||
| $inner_content = $blocks[ $i ]['innerContent']; | ||
| $blocks[ $i ]['innerBlocks'] = gutenberg_resolve_pattern_blocks( | ||
| $blocks[ $i ]['innerBlocks'] | ||
| ); | ||
| $blocks[ $i ]['innerContent'] = $inner_content; | ||
| $inner_content = $prev_inner_content; | ||
| } | ||
| ++$i; | ||
| } | ||
| } | ||
| return $blocks; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| <?php | ||
| /** | ||
| * WordPress 7.0 compatibility functions for the Gutenberg | ||
| * editor plugin changes related to REST API. | ||
| * | ||
| * @package gutenberg | ||
| */ | ||
|
|
||
| /** | ||
| * Retrieves a single unified template object using its id. | ||
| * Parses pattern blocks in the template content. | ||
| * | ||
| * @param WP_Block_Template|null $block_template The found block template, or null if there isn't one. | ||
| * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). | ||
| * @param string $template_type Template type. Either 'wp_template' or 'wp_template_part'. | ||
| */ | ||
| function gutenberg_parse_pattern_blocks_in_block_template( $block_template, $id, $template_type ) { | ||
| if ( 'wp_template' !== $template_type ) { | ||
| return $block_template; | ||
| } | ||
|
|
||
| if ( ! empty( $block_template->content ) ) { | ||
| $blocks = parse_blocks( $block_template->content ); | ||
| if ( ! empty( $blocks ) ) { | ||
| $blocks = gutenberg_resolve_pattern_blocks( $blocks ); | ||
| $block_template->content = serialize_blocks( $blocks ); | ||
| } | ||
| } | ||
| return $block_template; | ||
| } | ||
|
|
||
| add_filter( 'get_block_template', 'gutenberg_parse_pattern_blocks_in_block_template', 10, 3 ); | ||
| add_filter( 'get_block_file_template', 'gutenberg_parse_pattern_blocks_in_block_template', 10, 3 ); | ||
|
|
||
| /** | ||
| * Retrieves a list of unified template objects based on a query. | ||
| * Parses pattern blocks in the template content items. | ||
| * | ||
| * @param WP_Block_Template[] $query_result Array of found block templates. | ||
| * @param array $query { | ||
| * Arguments to retrieve templates. All arguments are optional. | ||
| * | ||
| * @type string[] $slug__in List of slugs to include. | ||
| * @type int $wp_id Post ID of customized template. | ||
| * @type string $area A 'wp_template_part_area' taxonomy value to filter by (for 'wp_template_part' template type only). | ||
| * @type string $post_type Post type to get the templates for. | ||
| * } | ||
| * @param string $template_type wp_template or wp_template_part. | ||
| */ | ||
| function gutenberg_parse_pattern_blocks_in_block_templates( $query_result, $query, $template_type ) { | ||
| if ( 'wp_template' !== $template_type ) { | ||
| return $query_result; | ||
| } | ||
|
|
||
| if ( ! empty( $query_result ) ) { | ||
| foreach ( $query_result as $template ) { | ||
| $blocks = parse_blocks( $template->content ); | ||
| if ( ! empty( $blocks ) ) { | ||
| $blocks = gutenberg_resolve_pattern_blocks( $blocks ); | ||
| $template->content = serialize_blocks( $blocks ); | ||
| } | ||
| } | ||
| } | ||
| return $query_result; | ||
| } | ||
|
|
||
| add_filter( 'get_block_templates', 'gutenberg_parse_pattern_blocks_in_block_templates', 10, 3 ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| <?php | ||
| /** | ||
| * Tests for the gutenberg_resolve_pattern_blocks function. | ||
| * | ||
| * @package WordPress | ||
| * @subpackage Blocks | ||
| */ | ||
|
|
||
| /** | ||
| * Test the gutenberg_resolve_pattern_blocks function. | ||
| * | ||
| * @group blocks | ||
| * @covers ::gutenberg_resolve_pattern_blocks | ||
| */ | ||
| class Gutenberg_Resolve_Pattern_Blocks_Test extends WP_UnitTestCase { | ||
|
|
||
| public function set_up() { | ||
| parent::set_up(); | ||
|
|
||
| register_block_pattern( | ||
| 'core/single-root', | ||
| array( | ||
| 'title' => 'Single Root Pattern', | ||
| 'content' => '<!-- wp:paragraph -->Single root content<!-- /wp:paragraph -->', | ||
| 'description' => 'A single root pattern.', | ||
| 'categories' => array( 'text' ), | ||
| ) | ||
| ); | ||
| register_block_pattern( | ||
| 'core/single-root-with-forbidden-chars-in-attrs', | ||
| array( | ||
| 'title' => 'Single Root Pattern<script>alert("XSS")</script>', | ||
| 'content' => '<!-- wp:paragraph -->Single root content<!-- /wp:paragraph -->', | ||
| 'description' => 'A single root pattern.<script>alert("XSS")</script><img src=x onerror=alert(1)>', | ||
| 'categories' => array( | ||
| 'text<script>alert("XSS")</script>', | ||
| 'bad\'); DROP TABLE wp_posts;--', | ||
| '<img src=x onerror=alert(1)>', | ||
| "evil\x00null\nbyte", | ||
| 'category with <strong>html</strong> tags', | ||
| ), | ||
| ) | ||
| ); | ||
| register_block_pattern( | ||
| 'core/with-attrs', | ||
| array( | ||
| 'title' => 'Pattern With Attrs', | ||
| 'content' => '<!-- wp:paragraph {"className":"custom-class"} -->Content<!-- /wp:paragraph -->', | ||
| 'description' => 'A pattern with existing attributes.', | ||
| ) | ||
| ); | ||
| register_block_pattern( | ||
| 'core/nested-single', | ||
| array( | ||
| 'title' => 'Nested Pattern', | ||
| 'content' => '<!-- wp:group --><!-- wp:paragraph -->Nested content<!-- /wp:paragraph --><!-- wp:pattern {"slug":"core/single-root"} /--><!-- /wp:group -->', | ||
| 'description' => 'A nested single root pattern.', | ||
| 'categories' => array( 'featured' ), | ||
| ) | ||
| ); | ||
| register_block_pattern( | ||
| 'core/existing-metadata', | ||
| array( | ||
| 'title' => 'Existing Metadata Pattern', | ||
| 'content' => '<!-- wp:paragraph {"metadata":{"patternName":"core/existing-metadata-should-not-overwrite","description":"A existing metadata pattern.","categories":["cake"]}} -->Existing metadata content<!-- /wp:paragraph -->', | ||
| ) | ||
| ); | ||
| } | ||
|
|
||
| public function tear_down() { | ||
| 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' ); | ||
| parent::tear_down(); | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * @dataProvider data_should_resolve_pattern_blocks_as_expected | ||
| * | ||
| * @param string $blocks A string representing blocks that need resolving. | ||
| * @param string $expected Expected result. | ||
| */ | ||
| public function test_should_resolve_pattern_blocks_as_expected( $blocks, $expected ) { | ||
| $actual = gutenberg_resolve_pattern_blocks( parse_blocks( $blocks ) ); | ||
| $this->assertSame( $expected, serialize_blocks( $actual ) ); | ||
| } | ||
|
|
||
| /** | ||
| * Data provider. | ||
| * | ||
| * @return array | ||
| */ | ||
| public function data_should_resolve_pattern_blocks_as_expected() { | ||
| return array( | ||
| // Resolves the single-root pattern and adds metadata. | ||
| 'single-root pattern' => array( | ||
| '<!-- wp:pattern {"slug":"core/single-root"} /-->', | ||
| '<!-- wp:paragraph {"metadata":{"patternName":"core/single-root","name":"Single Root Pattern","description":"A single root pattern.","categories":["text"]}} -->Single root content<!-- /wp:paragraph -->', | ||
| ), | ||
| // Existing attributes are preserved when adding metadata. | ||
| 'existing attributes preserved' => array( | ||
| '<!-- wp:pattern {"slug":"core/with-attrs"} /-->', | ||
| '<!-- wp:paragraph {"className":"custom-class","metadata":{"patternName":"core/with-attrs","name":"Pattern With Attrs","description":"A pattern with existing attributes."}} -->Content<!-- /wp:paragraph -->', | ||
| ), | ||
| // Resolves the nested single-root pattern and adds metadata. | ||
| 'nested single-root pattern' => array( | ||
| '<!-- wp:pattern {"slug":"core/nested-single"} /-->', | ||
| '<!-- wp:group {"metadata":{"patternName":"core/nested-single","name":"Nested Pattern","description":"A nested single root pattern.","categories":["featured"]}} --><!-- wp:paragraph -->Nested content<!-- /wp:paragraph --><!-- wp:paragraph {"metadata":{"patternName":"core/single-root","name":"Single Root Pattern","description":"A single root pattern.","categories":["text"]}} -->Single root content<!-- /wp:paragraph --><!-- /wp:group -->', | ||
| ), | ||
| // Sanitizes fields. | ||
| 'sanitized pattern attrs' => array( | ||
| '<!-- wp:pattern {"slug":"core/single-root-with-forbidden-chars-in-attrs"} /-->', | ||
| '<!-- wp:paragraph {"metadata":{"patternName":"core/single-root-with-forbidden-chars-in-attrs","name":"Single Root Pattern","description":"A single root pattern.","categories":["text","bad\'); DROP TABLE wp_posts;\u002d\u002d","","evil\u0000null byte","category with html tags"]}} -->Single root content<!-- /wp:paragraph -->', | ||
| ), | ||
| // Metadata is merged with existing metadata and existing metadata is preserved. | ||
| 'existing metadata preserved' => array( | ||
| '<!-- wp:pattern {"slug":"core/existing-metadata"} /-->', | ||
| '<!-- wp:paragraph {"metadata":{"patternName":"core/existing-metadata","name":"Existing Metadata Pattern","description":"A existing metadata pattern.","categories":["cake"]}} -->Existing metadata content<!-- /wp:paragraph -->', | ||
| ), | ||
| ); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.