Skip to content
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

Block Hooks: Apply to Post Content (on frontend and in editor) #67272

Merged
merged 10 commits into from
Dec 12, 2024
3 changes: 3 additions & 0 deletions backport-changelog/6.8/7898.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/7898

* https://github.com/WordPress/gutenberg/pull/67272
162 changes: 162 additions & 0 deletions lib/compat/wordpress-6.8/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,165 @@ function gutenberg_stabilize_experimental_block_supports( $args ) {
}

add_filter( 'register_block_type_args', 'gutenberg_stabilize_experimental_block_supports', PHP_INT_MAX, 1 );

function gutenberg_apply_block_hooks_to_post_content( $content ) {
// The `the_content` filter does not provide the post that the content is coming from.
// However, we can infer it by calling `get_post()`, which will return the current post
// if no post ID is provided.
return apply_block_hooks_to_content( $content, get_post(), 'insert_hooked_blocks' );
Copy link
Member

Choose a reason for hiding this comment

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

As simple as that? apply_block_hooks_to_content util is really powerful at this point. What risks do you see with this approach? What if there are synced patterns inside the rendered content? I assume they all are annotated with block hooks to ignore at this point, so double processing should be a non-issue if we ignore the additional CPU usage.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I was also really happy when I saw how straight-forward this was. apply_block_hooks_to_content was introduced by @tjcafferkey, and it really is a great high-level util to insert Block Hooks into a piece of block markup 😄

What risks do you see with this approach? What if there are synced patterns inside the rendered content? I assume they all are annotated with block hooks to ignore at this point, so double processing should be a non-issue if we ignore the additional CPU usage.

Yeah, intuitively I'd say this shouldn't duplicate any hooked blocks, but I do want to give it some good smoke testing to be sure.

}
// We need to apply this filter before `do_blocks` (which is hooked to `the_content` at priority 9).
add_filter( 'the_content', 'gutenberg_apply_block_hooks_to_post_content', 8 );

/**
* Hooks into the REST API response for the Posts endpoint and adds the first and last inner blocks.
*
* @since 6.6.0
* @since 6.8.0 Support non-`wp_navigation` post types.
*
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @return WP_REST_Response The response object.
*/
function gutenberg_insert_hooked_blocks_into_rest_response( $response, $post ) {
if ( empty( $response->data['content']['raw'] ) || empty( $response->data['content']['rendered'] ) ) {
return $response;
}

$attributes = array();
$ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
if ( ! empty( $ignored_hooked_blocks ) ) {
$ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
$attributes['metadata'] = array(
'ignoredHookedBlocks' => $ignored_hooked_blocks,
);
}

if ( 'wp_navigation' === $post->post_type ) {
$wrapper_block_type = 'core/navigation';
} else {
Copy link
Member

Choose a reason for hiding this comment

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

Noting that this else clause can safely assume that the post_type is either post or page based on the filters defined: rest_prepare_page or rest_prepare_post.

$wrapper_block_type = 'core/post-content';
}

$content = get_comment_delimited_block_content(
$wrapper_block_type,
$attributes,
$response->data['content']['raw']
);

$content = apply_block_hooks_to_content(
$content,
$post,
'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata'
);

// Remove mock block wrapper.
$content = remove_serialized_parent_block( $content );

$response->data['content']['raw'] = $content;

// No need to inject hooked blocks twice.
$priority = has_filter( 'the_content', 'apply_block_hooks_to_content' );
if ( false !== $priority ) {
remove_filter( 'the_content', 'apply_block_hooks_to_content', $priority );
}

/** This filter is documented in wp-includes/post-template.php */
$response->data['content']['rendered'] = apply_filters( 'the_content', $content );

// Add back the filter.
if ( false !== $priority ) {
add_filter( 'the_content', 'apply_block_hooks_to_content', $priority );
}

return $response;
}
add_filter( 'rest_prepare_page', 'gutenberg_insert_hooked_blocks_into_rest_response', 10, 2 );
add_filter( 'rest_prepare_post', 'gutenberg_insert_hooked_blocks_into_rest_response', 10, 2 );

/**
* Updates the wp_postmeta with the list of ignored hooked blocks
* where the inner blocks are stored as post content.
*
* @since 6.6.0
* @since 6.8.0 Support non-`wp_navigation` post types.
ockham marked this conversation as resolved.
Show resolved Hide resolved
* @access private
*
* @param stdClass $post Post object.
* @return stdClass The updated post object.
*/
function gutenberg_update_ignored_hooked_blocks_postmeta( $post ) {
/*
* In this scenario the user has likely tried to create a new post object via the REST API.
* In which case we won't have a post ID to work with and store meta against.
*/
if ( empty( $post->ID ) ) {
return $post;
}

/*
* Skip meta generation when consumers intentionally update specific fields
* and omit the content update.
*/
if ( ! isset( $post->post_content ) ) {
return $post;
}

/*
* Skip meta generation if post type is not set.
*/
if ( ! isset( $post->post_type ) ) {
return $post;
}

$attributes = array();

$ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
if ( ! empty( $ignored_hooked_blocks ) ) {
$ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
$attributes['metadata'] = array(
'ignoredHookedBlocks' => $ignored_hooked_blocks,
);
}

if ( 'wp_navigation' === $post->post_type ) {
$wrapper_block_type = 'core/navigation';
} else {
$wrapper_block_type = 'core/post-content';
}

$markup = get_comment_delimited_block_content(
$wrapper_block_type,
$attributes,
$post->post_content
);

$existing_post = get_post( $post->ID );
// Merge the existing post object with the updated post object to pass to the block hooks algorithm for context.
$context = (object) array_merge( (array) $existing_post, (array) $post );
$context = new WP_Post( $context ); // Convert to WP_Post object.
$serialized_block = apply_block_hooks_to_content( $markup, $context, 'set_ignored_hooked_blocks_metadata' );
$root_block = parse_blocks( $serialized_block )[0];

$ignored_hooked_blocks = isset( $root_block['attrs']['metadata']['ignoredHookedBlocks'] )
? $root_block['attrs']['metadata']['ignoredHookedBlocks']
: array();

if ( ! empty( $ignored_hooked_blocks ) ) {
$existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
if ( ! empty( $existing_ignored_hooked_blocks ) ) {
$existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true );
$ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) );
}

if ( ! isset( $post->meta_input ) ) {
$post->meta_input = array();
}
$post->meta_input['_wp_ignored_hooked_blocks'] = json_encode( $ignored_hooked_blocks );
}

$post->post_content = remove_serialized_parent_block( $serialized_block );
return $post;
}
add_filter( 'rest_pre_insert_page', 'gutenberg_update_ignored_hooked_blocks_postmeta' );
add_filter( 'rest_pre_insert_post', 'gutenberg_update_ignored_hooked_blocks_postmeta' );
23 changes: 23 additions & 0 deletions packages/block-library/src/post-content/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,33 @@ function render_block_core_post_content( $attributes, $content, $block ) {
$content .= wp_link_pages( array( 'echo' => 0 ) );
}

$ignored_hooked_blocks = get_post_meta( $post_id, '_wp_ignored_hooked_blocks', true );
Copy link
Member

Choose a reason for hiding this comment

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

The trick for the first and last child of the Post Content block works like a charm!

if ( ! empty( $ignored_hooked_blocks ) ) {
$ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
$attributes['metadata'] = array(
'ignoredHookedBlocks' => $ignored_hooked_blocks,
);
}

// Wrap in Post Content block so the Block Hooks algorithm can insert blocks
// that are hooked as first or last child of `core/post-content`.
$content = get_comment_delimited_block_content(
'core/post-content',
$attributes,
$content
);

// We need to remove the `core/post-content` block wrapper after the Block Hooks algorithm,
// but before `do_blocks` runs, as it would otherwise attempt to render the same block again --
// thus recursing infinitely.
add_filter( 'the_content', 'remove_serialized_parent_block', 8 );

/** This filter is documented in wp-includes/post-template.php */
$content = apply_filters( 'the_content', str_replace( ']]>', ']]>', $content ) );
unset( $seen_ids[ $post_id ] );

remove_filter( 'the_content', 'remove_serialized_parent_block', 8 );

if ( empty( $content ) ) {
return '';
}
Expand Down
Loading