Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .changeset/cold-doors-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
"@wpengine/wp-graphql-content-blocks": major
---

Replaced core/block with core/synced-pattern for reusable blocks, aligning with WP 6.3's synced patterns.

**🚨Breaking change🚨** This update does not break functionality for WP < 6.3, but it alters the GraphQL response structure.

Query:
```
{
posts {
nodes {
editorBlocks {
name
clientId
parentClientId
... on CoreSyncedPattern {
attributes {
slug
}
name
innerBlocks {
name
clientId
parentClientId
}
}
}
}
}
}
```
Response:
```
{
"data": {
"posts": {
"nodes": [
{
"editorBlocks": [
{
"name": "core/synced-pattern",
"clientId": "67b317909b801",
"parentClientId": null,
"attributes": {
"slug": "my-synced-pattern"
},
"innerBlocks": [
{
"name": "core/group",
"clientId": "67b317909b89a",
"parentClientId": null
}
]
},
{
"name": "core/group",
"clientId": "67b317909b89a",
"parentClientId": "67b317909b801"
}
]
}
]
}
}
}
```
21 changes: 16 additions & 5 deletions includes/Data/ContentBlocksResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ public static function resolve_content_blocks( $node, $args, $allowed_block_name
* @param array $allowed_block_names The list of allowed block names to filter.
*/
$parsed_blocks = apply_filters( 'wpgraphql_content_blocks_resolve_blocks', $parsed_blocks, $node, $args, $allowed_block_names );

return is_array( $parsed_blocks ) ? $parsed_blocks : [];
}

Expand All @@ -100,7 +99,6 @@ public static function resolve_content_blocks( $node, $args, $allowed_block_name
*/
private static function parse_blocks( $content ): array {
$blocks = parse_blocks( $content );

return self::handle_do_blocks( $blocks );
}

Expand Down Expand Up @@ -153,7 +151,6 @@ private static function handle_do_block( array $block ): ?array {
$block = self::populate_reusable_blocks( $block );
$block = self::populate_pattern_inner_blocks( $block );
$block = self::populate_navigation_blocks( $block );

// Prepare innerBlocks.
if ( ! empty( $block['innerBlocks'] ) ) {
$block['innerBlocks'] = self::handle_do_blocks( $block['innerBlocks'] );
Expand All @@ -173,6 +170,11 @@ private static function is_block_empty( array $block ): bool {
return false;
}

if ( isset( $block['attrs']['ref'] ) ) {
// If 'ref' exists, it's likely a synced pattern, so we don't consider it empty.
return false;
}

// If there is no innerHTML and no innerContent, we can consider it empty.
if ( empty( $block['innerHTML'] ) && empty( $block['innerContent'] ) ) {
return true;
Expand Down Expand Up @@ -294,8 +296,11 @@ private static function populate_reusable_blocks( array $block ): array {
if ( empty( $parsed_blocks ) ) {
return $block;
}

return array_merge( ...$parsed_blocks );
// Wrap it in a core/synced-pattern block instead of flattening
$block['blockName'] = 'core/synced-pattern';
$block['attrs']['slug'] = $reusable_block->post_name;
$block['innerBlocks'] = $parsed_blocks;
return $block;
}

/**
Expand Down Expand Up @@ -380,6 +385,12 @@ private static function filter_allowed_blocks( array $blocks, array $allowed_blo
return array_filter(
$blocks,
static function ( $block ) use ( $allowed_block_names ) {
// Allow 'core/synced-pattern' to pass through without filtering.
// We need to ensure this block is included regardless of the allowed block names
// because it's handled separately (e.g., it should be rendered but not manually inserted by the user).
if ( 'core/synced-pattern' === $block['blockName'] ) {
return true;
}
return in_array( $block['blockName'], $allowed_block_names, true );
}
);
Expand Down
36 changes: 36 additions & 0 deletions includes/Registry/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public function init(): void {
$this->register_interface_types();
$this->register_scalar_types();
$this->register_support_block_types();
$this->register_custom_block_types();
$this->register_block_types();
}

Expand Down Expand Up @@ -257,6 +258,41 @@ static function ( $post_type ) {
}//end foreach
}

/**
* Registers custom block types.
*
* The 'core/synced-pattern' block is a reusable synced pattern block and
* is not meant to appear in the block editor selection but is used
* internally for resolving reusable content.
*/
protected function register_custom_block_types(): void {
$registry = \WP_Block_Type_Registry::get_instance();
$block_name = 'core/synced-pattern';

if ( ! $registry->is_registered( $block_name ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems very fragile and maybe also unnecessary?
Instead I would recommend reusing CoreBlock by renaming it in GraphQL to CoreSyncedPattern

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justlevine How can we rename an existing block in GraphQL? I'm not familiar with this.

Copy link
Member Author

@theodesp theodesp Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@justlevine how should we handle the mapping in GraphQL to reflect core/synced-pattern without re-registering it?

Copy link
Contributor

@justlevine justlevine Mar 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/wp-graphql/wp-graphql/blob/eeff88cde5ef09095f4718d48cde534c9ba80a55/src/Type/WPObjectType.php#L85

Pseudocode:

add_filter( 'graphql_type_name', static function( $type_name ) {
  // Bail if not our Block. (str_starts_with() would probably be too loose).
  if ( 'CoreBlock' !== $type_name && 'CoreBlockAttributes' !== $type_name ) { return $type_name; }
  
  // or to whatever you're calling it.
  return str_replace( 'CoreBlock', 'CoreSyncedPatternBlock', $type_name );
} );

Or you could probably use includes/Block/CoreBlock.php to intercept and overload the default registration.

$registry->register(
$block_name,
[
'name' => $block_name,
'title' => __( 'Synced Pattern', 'wp-graphql-content-blocks' ),
'icon' => null,
'category' => 'theme',
'attributes' => [
'ref' => [
'type' => 'number',
],
'slug' => [
'type' => 'string',
],
],
'render_callback' => static function ( $attributes, $content ) {
return $content;
},
]
);
}
}

/**
* Register Scalar types to the GraphQL Schema
*
Expand Down
14 changes: 10 additions & 4 deletions tests/unit/ContentBlocksResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,16 @@ public function tearDown(): void {
public function test_resolve_content_blocks_resolves_reusable_blocks() {
$post_model = new Post( get_post( $this->reusable_post_id ) );
$actual = $this->instance->resolve_content_blocks( $post_model, [ 'flat' => true ] );

// There should return only the non-empty blocks
$this->assertEquals( 3, count( $actual ) );
$this->assertEquals( 'core/columns', $actual[0]['blockName'] );

$this->assertNotEmpty( $actual );
$this->assertEquals( 'core/synced-pattern', $actual[0]['blockName'] );

$this->assertArrayHasKey( 'attrs', $actual[0] );
$this->assertArrayHasKey( 'ref', $actual[0]['attrs'] );
$this->assertArrayHasKey( 'slug', $actual[0]['attrs'] );

$this->assertEquals( 'core/columns', $actual[1]['blockName'] );
$this->assertCount( 4, $actual );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now count the 'synced pattern` block as well.

}

public function test_resolve_content_blocks_filters_empty_blocks() {
Expand Down
Loading