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

Conversation

ockham
Copy link
Contributor

@ockham ockham commented Nov 25, 2024

What?

Apply Block Hooks to post content -- i.e., insert hooked blocks into post content.

Why?

We've seen some demand for this. A typical example would be a plugin that provides blocks that can be used in posts and that would like to provide extensibility for those blocks.

How?

It works the same way as in templates:

  • If a post is loaded on the frontend, Block Hooks will be applied to the post content, thus inserting hooked blocks.
  • If it is loaded in the editor, the Posts endpoint will inject both the hooked blocks, and the ignoredHookedBlocks metadata (into anchor blocks).
  • This means that if the post is modified in the editor -- including moving or deleting of hooked blocks -- it will have that ignoredHookedBlocks metadata on individual anchor blocks persisted when saving.
  • Thus, when loading the post again on the frontend, no hooked blocks will be inserted next to anchor blocks that list them in their ignoredHookedBlocks. OTOH, any hooked blocks that were present when saving the post have now simply become part of the markup.

Testing Instructions, Part One

Start by installing the following plugin, but do not activate it yet:

Plugin code
<?php
/**
 * Plugin Name:       Insert Separator blocks Before Headings.
 * Description:       Block Hooks demo plugin that inserts Separator blocks before Heading blocks.
 * Version:           0.1.0
 * Requires at least: 6.7
 */

defined( 'ABSPATH' ) || exit;

function insert_separators_before_headings( $hooked_blocks, $position, $anchor_block, $context ) {
	if ( ! $context instanceof WP_Post ) {
		return $hooked_blocks;
	}

	if ( $anchor_block === 'core/heading' && $position === 'before' ) {
		$hooked_blocks[] = 'core/separator';
	}

	return $hooked_blocks;
}
add_filter( 'hooked_block_types', 'insert_separators_before_headings', 10, 4 );

function set_separator_block_inner_html( $hooked_block, $hooked_block_type, $relative_position, $anchor_block ) {
	if ( $anchor_block['blockName'] === 'core/heading' && 'before' === $relative_position ) {
        $hooked_block['innerContent'] = array( '<hr class="wp-block-separator has-alpha-channel-opacity"/>' );
	}

	return $hooked_block;
}
add_filter( 'hooked_block_core/separator', 'set_separator_block_inner_html', 10, 4 );
  • Create a new post that contains a number of headings and paragraphs.
  • Save that post, and view it on the frontend. Keep it open in a tab.
  • Activate the plugin.
  • Go back to the tab with the post (on the frontend). Reload.
  • Verify that before each heading, a separator (i.e. a horizontal line) has been inserted.
  • Open the post in the editor, and verify that the separators are also present there.
  • Modify the separators -- e.g. move one of them around and delete another. Save the post again.
  • Reload it on the frontend again. Verify that the changes you made in the editor are respected.

Screenshots or screencast

block-hooks-post-content

Testing Instructions, Part Two

Edit: The behavior covered by the following testing instructions changed after I wrote them, as @gziolo noticed here. We ended up deciding that the new behavior -- where hooked blocks would not be inserted if the anchor block was added after the post was added might be preferable.

  • Now edit the post, and insert another heading at the bottom. Note that no separator is inserted at the client side "right away" -- the reason is that Block Hooks are almost exclusively handled on the server side. This is one minor UX inconsistency -- the same inconsistency is present in templates.
  • Save the post, and reload it on the frontend. Note that a separator has now been inserted before the newly added heading!
  • Finally, reload the post in the editor. Note that the separator is now also present there.

Screenshots or screencast, Part Two

block-hooks-post-content-additonal-block

// 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.

@ockham
Copy link
Contributor Author

ockham commented Nov 26, 2024

I've filed a backport (WordPress/wordpress-develop#7889).

I'm also experimenting with an alternative in WordPress/wordpress-develop#7898 (easier to do in Core than in GB), which will insert hooked blocks both on the frontend and in the editor. Maybe we can get that to work after all.

@ockham ockham force-pushed the update/apply-block-hooks-to-post-content branch from 5b5d0e8 to 9931b82 Compare December 2, 2024 13:38
@ockham ockham force-pushed the update/apply-block-hooks-to-post-content branch from 9931b82 to e057209 Compare December 3, 2024 13:02
@ockham ockham changed the title Block Hooks: Apply to Post Content (on frontend) Block Hooks: Apply to Post Content (on frontend and in editor) Dec 3, 2024
Copy link

github-actions bot commented Dec 3, 2024

Flaky tests detected in 705d3f6.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/12140570324
📝 Reported issues:

Copy link

github-actions bot commented Dec 4, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: ockham <[email protected]>
Co-authored-by: gziolo <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


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.

@gziolo
Copy link
Member

gziolo commented Dec 11, 2024

Testing Instructions, Part One

Everything works as advertised 👍🏻

Testing Instructions, Part Two

  • Now edit the post, and insert another heading at the bottom. Note that no separator is inserted at the client side "right away" -- the reason is that Block Hooks are almost exclusively handled on the server side. This is one minor UX inconsistency -- the same inconsistency is present in templates.
  • Save the post, and reload it on the frontend. Note that a separator has now been inserted before the newly added heading! ✨
  • Finally, reload the post in the editor. Note that the separator is now also present there.

In my testing, the Separator doesn't get prepended to the Heading block in the case when I add it to the post that previously has been saved with processed Heading blocks. What could I do differently?

Screen.Recording.2024-12-11.at.14.45.20.mov

Could it be related to _wp_ignored_hooked_blocks? My understanding is that once it's set for the post or page, we can safely assume it was a user's decision to exclude the Separator block. I think it works correctly in that regard.

@gziolo
Copy link
Member

gziolo commented Dec 11, 2024

I'm not entirely sure if it's in any way related to these changes, but the toggle in the Plugins section in the sidebar doesn't work well with the test example:

Screenshot 2024-12-11 at 15 03 15 Screenshot 2024-12-11 at 15 03 27

The Separator block gets injected after the Heading block instead of before.

@gziolo
Copy link
Member

gziolo commented Dec 11, 2024

I still want to play with synced patterns and template parts when served from the database so they essentially behave like WP_Post.

@@ -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!

Copy link
Member

@gziolo gziolo left a comment

Choose a reason for hiding this comment

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

Everything looks good to me. It works excellently for posts and pages.

FWIW, for the Core version of this change, I've actually been considering if we should apply Block Hooks directly from inside the Posts controller, rather than adding the filter to rest_prepare_page and rest_prepare_post, see WordPress/wordpress-develop#7898 (comment):

The only remaining question for Gutenberg implementation is whether we should explicitly limit the functionality to post and page post types for now and cover all custom post types in WP core by applying changes directly in the Posts controller? I would be in favor of that as otherwise, it might have unintended consequences for custom post types with the Gutenberg plugin installed.

@ockham
Copy link
Contributor Author

ockham commented Dec 12, 2024

In my testing, the Separator doesn't get prepended to the Heading block in the case when I add it to the post that previously has been saved with processed Heading blocks. [...]

Could it be related to _wp_ignored_hooked_blocks?

Ah yes, good spot! Apologies, I think that this change in behavior is a side effect of some code I pushed after I wrote the testing instructions.

It is, as you guessed correctly, code that was needed to make firstChild/lastChild insertion into Post Content blocks work. Essentially, we run Block Hooks -- including the logic that sets ignoredHookedBlocks -- upon save; and it's not just limited to post meta (i.e. _wp_ignored_hooked_blocks), but also includes the metadata we set on anchor blocks. This means that a post that has a newly inserted anchor block is saved with the ignoredHookedBlocks metadata set on that anchor block, leading to the behavior you were seeing.

My understanding is that once it's set for the post or page, we can safely assume it was a user's decision to exclude the Separator block. I think it works correctly in that regard.

Huh 🤔 I think you have a point there. It's actually not bad in terms of UX -- a bit more predictable maybe. I'm happy to just consider it a happy accident and land it as-is.

(If needed, we can still tweak the behavior later, based on user feedback.)

@ockham
Copy link
Contributor Author

ockham commented Dec 12, 2024

I'm not entirely sure if it's in any way related to these changes, but the toggle in the Plugins section in the sidebar doesn't work well with the test example:

Screenshot 2024-12-11 at 15 03 15 Screenshot 2024-12-11 at 15 03 27
The Separator block gets injected after the Heading block instead of before.

Yeah, that's a known issue. I filed a Trac ticket for this a while ago.

The tl;dr is that the toggle uses the ignoredHookedBlocks metadata on the anchor block to determine if there's a hooked block. However, there's no information about the hooked block's relative position, so we currently simply default to after. One possible fix would be to include that information, i.e. to change the format of ignoredHookedBlocks from a simple array of (ignored) hooked block types (e.g. [ "core/separator" ] to an associative array, where the keys are hooked block types, and the values are their positions, e.g. { "core/separator": "before" }. See the Trac ticket I linked for more information.

@ockham
Copy link
Contributor Author

ockham commented Dec 12, 2024

Everything looks good to me. It works excellently for posts and pages.

Great! 🎉 Thank you very much for reviewing 😊

FWIW, for the Core version of this change, I've actually been considering if we should apply Block Hooks directly from inside the Posts controller, rather than adding the filter to rest_prepare_page and rest_prepare_post, see WordPress/wordpress-develop#7898 (comment):

The only remaining question for Gutenberg implementation is whether we should explicitly limit the functionality to post and page post types for now and cover all custom post types in WP core by applying changes directly in the Posts controller? I would be in favor of that as otherwise, it might have unintended consequences for custom post types with the Gutenberg plugin installed.

Yeah, I agree 100% -- it's best if we limit the Gutenberg version to post and page 👍

@ockham ockham merged commit 3de8dfb into trunk Dec 12, 2024
63 checks passed
@ockham ockham deleted the update/apply-block-hooks-to-post-content branch December 12, 2024 15:50
@github-actions github-actions bot added this to the Gutenberg 20.0 milestone Dec 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Block hooks [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants