Block Hooks API & The Core Navigation Block

Back in November it was announced that Woo will be dedicating resources (aka moi) to help advance the Block Hooks API in WordPress while validating, testing, and implementing its usage in WooCommerce.

Lately I’ve been working on enabling the core Navigation block to have inner blocks auto-inserted via the Block Hooks API. Why? This was one of the most requested features for the API, and was something we could utilise in Woo (think inserting Customer Account, Mini Cart blocks etc into the Navigation) so it was also in our interest to help get this included in WP 6.5. After multiple attempts, approaches and discussions it’s now been merged so I’m here to document how it works, and what I’ve learned working on this.

The requirements

The current implementation of the Block Hooks API was limited to inserting your hooked blocks relative to the specified anchor block on templates, template parts and patterns. For this task, we wanted to allow users to insert a first_child and/or last_child hooked block inside the Navigation block as inner blocks.

See Trac ticket here.

The uniqueness of the Navigation block

The Navigation block works slightly differently to your typical block. For example, the block itself and its inner blocks are completely detached from each other.

Let me show you an example of the block markup for the Navigation block.

<!-- wp:navigation {"ref":4} /-->
HTML

The attribute ref refers to a post ID in the wp_posts table where the inner block markup is stored.

Note: This entry also has a post_type of wp_navigation.

To make things a little more complicated, the Navigation block also has various fallback mechanisms. If the referenced post ID does not exist in the database it ensures a navigation of some sort always tries to render. We needed to account for this.

On top of all this there are various other things that make the Navigation block unique:

  1. It renders its children in <ul></ul> markup.
  2. Not all blocks are allowed to be children of the Navigation block.
    • Our implementation pretty much ignores this, but more on that below.
  3. It needs to account for various types of navigations (sub menus, responsive menus etc)

Challenges we faced

In order to implement this in an elegant way we needed to overcome some of the below challenges.

  • Since this is the first individual block that’s being made compatible with the API (at least significant change AFAIK) we need to find the most appropriate entry point for it, on both the editor side (REST API) and the server-side.
    • This is made particularly difficult because of the relationship between the block and its children, outlined above.
  • If the Navigation block and it’s inner blocks are detached from each other and multiple instances of the same navigation can exist. How are we going to track ignoredHookedBlocks and sync it between instances?
    • The purpose of ignoredHookedBlocks is to ensure that your hooked block isn’t inserted more times than it should be. For this to work, every hooked block is stored in an attribute on the anchor block (If you want to understand how this works, you can read a blog post I wrote on it here). In short, if the inner blocks are not aware of their parent block, how do we know whether its safe to insert or not?
  • Within the Editor, inner blocks are fetched via the REST API whereas on the frontend it’s server-side rendered.

Our solution

We chose two main ‘entry points’ to insert our hooked blocks. On the Editor side it was in the REST API when the navigation block was fetching its own inner blocks. On the frontend we directly updated the class responsible for rendering the navigation markup. However there was still a problem, in each of these cases we had access to the navigation ID, and its inner blocks but not the Navigation block itself so we still had to solve the problem of tracking ignoredHookedBlocks.

The Editor

The rest_prepare_wp_navigation hook is executed when the navigation inner blocks are being prepared to be sent in a response back to the Editor for rendering during the editing experience. It’s here we are able to insert our hooked blocks.

The Frontend

For the frontend we made changes directly to the WP_Navigation_Block_Renderer class to ensure that whether we’re rendering the intended navigation or the fallback navigation, we inserted our hooked blocks before they were rendered.

Tracking ignoredHookedBlocks

Since the navigation is stored in the database as a post, we were able to track ignoredHookedBlocks as wp_postmeta against each navigation, which is updated any time a navigation is saved. This is required because of two reasons:

  1. We don’t get access (easily, anyway) to the anchor block where this data is typically stored as attribute data. We do get access to the post ID of the database entry where the inner blocks are stored though.
  2. Sites can have the same navigation (e.g. ref) in separate locations (posts, pages, templates etc) on their site meaning if this data was to be stored as attribute data we would need to keep each instance in sync every time one of them was saved since they fundamentally should be rendering the same navigation.

Doing it this way means that every time the navigation post is queried in the database we also get its post meta.

This approach is something Bernie Reiter came up with and works the best out of various approaches we tried.

Utilising core functionality

Instead of rewriting the logic to parse, traverse and insert our inner blocks we wanted to utilise functionality already written which was being used in core. These functions required our anchor block and its attribute data (for ignoredHookedBlocks) which was something we didn’t have.

To workaround this, since we only had the inner blocks, and post ID at the point of execution we decided on mocking a parent Navigation block after a lookup in the wp_postmeta table for our ignoredHookedBlocks data to add to it.

After this, all we needed to return was our inner blocks and the rendering process took care of the rest.

Further notes

  • ignoredHookedBlocks are ignored from being inserted in all positions. This was decided here, it would be interesting to know how other developers may use this API to understand if this is still the right decision.
    • Scenario: If Plugin A inserts a hooked block after its anchor block, then Plugin B wants to insert the same hooked block before the anchor block. Plugin B’s will be ignored AFAIK?
  • Support for allowedChildren in block.json was worked on (PR here) soon after. Supporting this in the API would be a great addition I think. More on this feature detailed in its dedicated issue.
  • Where possible, leaning on core functionality did a lot of the heavy lifting, maintained consistency and has necessary coverage to increase confidence.

Conclusion

Even though it took some time to reach our solution, the result was a surprisingly small amount of code all of which was limited to a single file. This task has helped me gain a deeper understanding of the technical implementation of our blocks, as well as the Block Hooks API. Big thanks to Bernie Reiter, who was always there to review my code, thoughts and ideas as well as provide extremely useful feedback at each stage.


You can view the final PR here.

One response to “Block Hooks API & The Core Navigation Block”

  1. […] wrote a more detailed post about implementing Block Hooks for the Navigation block that covers the specific challenges we faced, the entry points we chose, and how we solved the […]

Leave a Reply to Building the Block Hooks API: My WordPress Core Experience – Code by Tom Cancel reply

Your email address will not be published. Required fields are marked *