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) #7898

Open
wants to merge 20 commits into
base: trunk
Choose a base branch
from

Conversation

ockham
Copy link
Contributor

@ockham ockham commented Nov 26, 2024

Why does this work? And 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 @sirreal 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, see this comment on the companion GB PR.

  • 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


Trac ticket: https://core.trac.wordpress.org/ticket/61074


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@ockham ockham self-assigned this Nov 26, 2024
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • The Plugin and Theme Directories cannot be accessed within Playground.
  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Comment on lines +1315 to +1323
$content = apply_block_hooks_to_content(
$content,
$post,
'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata'
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're now inserting hooked blocks and setting ignoredHookedBlocks metadata. Previously, we were only inserting hooked blocks (i.e. the default behavior of apply_block_hooks_to_content when no third argument is given).

This change also affects the Navigation block. We need to make sure that any hooked blocks inside of the Navigation block -- both as first/last child of core/navigation itself, but also after/before blocks inside of the Navigation block -- still work (both on the frontend and in the editor.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To test if the Navigation block still works, we can follow the testing instructions from WordPress/gutenberg#59021.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out there was an unrelated issue with this. I've filed a fix: #7941

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've committed #7941 to Core [62639], and rebased this PR to include the fix.

@ockham ockham marked this pull request as ready for review November 27, 2024 19:53
Copy link

github-actions bot commented Nov 27, 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.

Core Committers: Use this line as a base for the props when committing in SVN:

Props bernhard-reiter, gziolo, jonsurrell, karolmanijak, leewillis77.

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

@ockham ockham requested review from mtias and gziolo November 27, 2024 19:53
@ockham
Copy link
Contributor Author

ockham commented Nov 27, 2024

FYI @kmanijak. Maybe you can give this a try and let me know if it works for the purpose you're interested in! 😊

src/wp-includes/blocks.php Outdated Show resolved Hide resolved
@@ -192,6 +192,7 @@
add_filter( 'the_title', 'convert_chars' );
add_filter( 'the_title', 'trim' );

add_filter( 'the_content', 'apply_block_hooks_to_content', 8 ); // BEFORE do_blocks().
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Quoting #7889 (comment):

apply_block_hooks_to_content parses blocks, traverses the block tree (to insert hooked blocks), and reserializes them. Note that it's followed immediately by do_blocks (on the line below), which also parses blocks.

This means that in theory, there's some potential for optimization (parse only once inside of do_blocks, and traverse to insert hooked blocks there).

In practice, I'm not sure if that's viable, or if that might cause some collisions; in particular if do_blocks is already used in other contexts where hooked blocks are already inserted (through a different mechanism), e.g. in templates or wp_navigation posts.

@kmanijak
Copy link

FYI @kmanijak. Maybe you can give this a try and let me know if it works for the purpose you're interested in! 😊

Thank you @ockham for working on this! 🚀 I gave it a try and at first glance it works as expected! I can achieve the goals raised in scope of the discussion in WooCommerce. I'll play with it more tomorrow to check if I see any problems.

One potential feature I can already see (but obviously not the scope for this PR!) is the ability for more strict selectors. For example: $anchor_block as core/post-title but only within Query Loop, not any core/post-title, including the main post title. This very specific example can be workaround by checking if ($context instanceof WP_Post) { but wanted to give some generic example.

Anyway, looking good, thank you! 🙌

@leewillis77
Copy link

Just following up on @kmanijak's comments. We flagged this as an issue to the Woo team, specifically about hooking blocks into Woo's Product Collection block when it was included in pages. Unfortunately, the patch here doesn't appear to resolve the issues we were seeing (we're unable to add hooked blocks inside the Product Collection block).

It's possible that the issue is Woo-specific (or on our side), but I just wanted to drop my observations here in case it is fundamental to the solution.
woocommerce/woocommerce#44776 (reply in thread)

@leewillis77
Copy link

I'm testing with the WP 6.7.1 with the patch from this issue. Are there other dependent changes to make this work (ie would I have to start with WP trunk?)

@ockham
Copy link
Contributor Author

ockham commented Dec 2, 2024

Thanks a lot for testing, @leewillis77! I'm a bit puzzled as to why this wouldn't work if patched against 6.7.1; there shouldn't really be any dependent changes required for this.

Can you maybe try with the playground link (from this comment)?

@ockham
Copy link
Contributor Author

ockham commented Dec 2, 2024

For reviewers: @kmanijak has kindly tested this using the Woo Product Collection block and my Like Button block in a number of different scenarios 🙂

@leewillis77
Copy link

@ockham @kmanijak I've re-tested this today (fresh install, re-patch etc.), and can confirm that block hooks are fired correctly on posts/pages. No idea why we couldn't get this working last week.

However, trying our actual implementation rather than just artificial tests I don't think this is going to give us what we actually need (what was originally raised on the WooCommerce discussion) due to the lack of contextual information (as @kmanijak alluded to in #7898 (comment)).

Ideally what we need in block hooks when we're deciding whether or not to hook a block in or not is the context of where it is. This is the same issue I originally raised in WordPress/gutenberg#54904 (comment). It's particularly bad here as the $context isn't even the top-level block, but a WP_Post object so we don't even have any idea what top-level block is being rendered so that we can make an informed decision about whether we should be hooking in or not.

src/wp-includes/blocks.php Outdated Show resolved Hide resolved
src/wp-includes/blocks.php Show resolved Hide resolved
@@ -1286,22 +1293,44 @@ function insert_hooked_blocks_into_rest_response( $response, $post ) {
'ignoredHookedBlocks' => $ignored_hooked_blocks,
);
}

if ( 'wp_navigation' === $post->post_type ) {
Copy link
Member

Choose a reason for hiding this comment

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

What about synced patterns wp/block? Should it be extensible? These post types are special do maybe it’s fine as is but a filter here could be useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, good point. I guess the wp_block post type should map to core/block.

A filter might make sense here 👍 Something to map post types to block types. (I wonder if we already have a function or data structure to do that somewhere 🤔 )

src/wp-includes/default-filters.php Show resolved Hide resolved
Comment on lines +767 to +770
add_filter( 'rest_prepare_page', 'insert_hooked_blocks_into_rest_response', 10, 2 );
add_filter( 'rest_prepare_post', 'insert_hooked_blocks_into_rest_response', 10, 2 );
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This follows the precedent of rest_prepare_wp_navigation below. However, it has the unfortunate side effect that Block Hooks aren't automatically applied to user-defined CPTs; those would need to be added manually (via add_filter( 'rest_prepare_my_cpt', ... )). (AFAIK, there's no "generic" rest_prepare filter that could cover all post types.)

I wonder if we should run insert_hooked_blocks_into_rest_response from inside the Posts controller instead, in order to insert hooked blocks into every CPT that has show_in_rest set to true 🤔 cc/ @gziolo

@ockham ockham force-pushed the update/apply-block-hooks-to-post-content-and-editor branch from ff37889 to 7635d23 Compare December 4, 2024 12:08
@ockham
Copy link
Contributor Author

ockham commented Dec 4, 2024

@gziolo Since insertion as the Post Content block's first or last child required changes to that block's PHP, I've updated the Gutenberg PR -- it also includes the required compat layer changes backported from this PR. Would you mind reviewing giving the GB PR a look?

There are a few open questions on this PR:

However, I don't think that those should be blockers for the Gutenberg PR. Both of them deserve some more consideration, but we might do that as part of this PR -- possibly even after the GB PR has landed 😊

@sirreal
Copy link
Member

sirreal commented Dec 11, 2024

I was following the testing instructions from the description and when I add more headings the separator is not being added 😕

Testing Instructions, Part Two

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

@sirreal
Copy link
Member

sirreal commented Dec 11, 2024

I was following the testing instructions from the description and when I add more headings the separator is not being added 😕

When I add more headers they all look like this, which I believe is not intended according to the testing instructions:

<!-- wp:heading {"level":1,"metadata":{"ignoredHookedBlocks":["core/separator"]}} -->
<h1 class="wp-block-heading">text</h1>
<!-- /wp:heading -->

@ockham
Copy link
Contributor Author

ockham commented Dec 12, 2024

@sirreal Good spot! Turns out @gziolo noticed the same thing over at the GB PR. I replied there. The tl;dr is

Apologies, I think that this change in behavior is a side effect of some code I pushed after I wrote the testing instructions.
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.

So we might keep it this way. I'll add a note to the testing instructions.

Copy link
Member

@sirreal sirreal left a comment

Choose a reason for hiding this comment

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

This tests well for me. I'm not well versed in block hooks but the changes seem reasonable. I've left a few notes, but I think this can be merged if you're confident.

Comment on lines +1057 to +1059
if ( null === $context ) {
$context = get_post();
}
Copy link
Member

Choose a reason for hiding this comment

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

Could this still be null and would that be a problem? null is in the return type of get_post().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, good question 🤔 The Block Hooks algorithm should work even with no $context specified -- it started out as an optional argument that filters such as hooked_block_types could use to conditionally insert hooked blocks based on context.

I think that that should still be the case, but I might wanna double-check 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems fine. I've added test coverage in a7693b0.

src/wp-includes/blocks.php Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants