Block Hooks (Hooked Blocks)
Block hooks automatically insert blocks at specified positions relative to other blocks, without modifying templates directly.
Basic Usage
In block.json:
{
"name": "my-plugin/newsletter-signup",
"blockHooks": {
"core/post-content": "after"
}
}
This automatically inserts the newsletter signup block after every post content block.
Hook Positions
| Position | Description |
|---|---|
before |
Insert before the target block |
after |
Insert after the target block |
firstChild |
Insert as first child inside target |
lastChild |
Insert as last child inside target |
Multiple Hooks
Hook to multiple blocks:
{
"blockHooks": {
"core/post-content": "after",
"core/post-excerpt": "after",
"core/query": "lastChild"
}
}
Hook Examples
After Post Content
{
"name": "my-plugin/share-buttons",
"blockHooks": {
"core/post-content": "after"
}
}
Before Navigation
{
"name": "my-plugin/announcement-bar",
"blockHooks": {
"core/navigation": "before"
}
}
Inside Header (First Child)
{
"name": "my-plugin/site-notice",
"blockHooks": {
"core/group": "firstChild"
}
}
Note: firstChild and lastChild require the target block to have InnerBlocks support.
Inside Footer (Last Child)
{
"name": "my-plugin/back-to-top",
"blockHooks": {
"core/template-part": "lastChild"
}
}
Conditional Hooks with PHP
Use the hooked_block_types filter for conditional insertion:
add_filter( 'hooked_block_types', function( $hooked_blocks, $position, $anchor_block, $context ) {
// Only add to single posts
if ( is_singular( 'post' ) && 'core/post-content' === $anchor_block && 'after' === $position ) {
$hooked_blocks[] = 'my-plugin/related-posts';
}
return $hooked_blocks;
}, 10, 4 );
Context-Aware Hooks
add_filter( 'hooked_block_types', function( $hooked_blocks, $position, $anchor_block, $context ) {
// $context is a WP_Block_Template or WP_Post object
// Only in specific templates
if ( $context instanceof WP_Block_Template ) {
if ( 'single' === $context->slug && 'after' === $position && 'core/post-content' === $anchor_block ) {
$hooked_blocks[] = 'my-plugin/author-bio';
}
}
return $hooked_blocks;
}, 10, 4 );
Remove Default Hooks
add_filter( 'hooked_block_types', function( $hooked_blocks, $position, $anchor_block, $context ) {
// Remove a hooked block
if ( 'core/post-content' === $anchor_block && 'after' === $position ) {
$key = array_search( 'my-plugin/newsletter', $hooked_blocks, true );
if ( false !== $key ) {
unset( $hooked_blocks[ $key ] );
}
}
return $hooked_blocks;
}, 10, 4 );
Modifying Hooked Block Attributes
Use the hooked_block_{block_name} filter:
add_filter( 'hooked_block_my-plugin/newsletter-signup', function( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block ) {
// Modify attributes based on anchor block
if ( 'core/post-content' === $parsed_anchor_block['blockName'] ) {
$parsed_hooked_block['attrs']['variant'] = 'inline';
}
return $parsed_hooked_block;
}, 10, 4 );
Dynamic Attributes
add_filter( 'hooked_block_my-plugin/share-buttons', function( $parsed_hooked_block ) {
// Add current post URL
$parsed_hooked_block['attrs']['shareUrl'] = get_permalink();
$parsed_hooked_block['attrs']['shareTitle'] = get_the_title();
return $parsed_hooked_block;
} );
User Control Over Hooked Blocks
Users can remove hooked blocks from the Site Editor:
- Open Site Editor → Templates
- Edit the template
- Select the hooked block
- Delete it
Once removed, WordPress stores this preference and won’t auto-insert again.
Detecting User Modifications
add_filter( 'hooked_block_types', function( $hooked_blocks, $position, $anchor_block, $context ) {
// Check if user has modified this template
if ( $context instanceof WP_Block_Template && $context->wp_id ) {
// Template has been customized, respect user's changes
// The block will only appear if not explicitly removed
}
return $hooked_blocks;
}, 10, 4 );
Complete Example: Related Posts Block
block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/related-posts",
"title": "Related Posts",
"category": "widgets",
"attributes": {
"numberOfPosts": {
"type": "number",
"default": 3
},
"showThumbnails": {
"type": "boolean",
"default": true
}
},
"blockHooks": {
"core/post-content": "after"
},
"supports": {
"align": [ "wide", "full" ],
"spacing": {
"margin": true,
"padding": true
}
},
"editorScript": "file:./index.js",
"style": "file:./style.css",
"render": "file:./render.php"
}
Conditional Registration
// Only hook on single posts
add_filter( 'hooked_block_types', function( $hooked_blocks, $position, $anchor_block, $context ) {
if ( 'core/post-content' !== $anchor_block || 'after' !== $position ) {
return $hooked_blocks;
}
// Remove from our block.json default
$key = array_search( 'my-plugin/related-posts', $hooked_blocks, true );
if ( false !== $key ) {
unset( $hooked_blocks[ $key ] );
}
// Only add on single posts
if ( is_singular( 'post' ) ) {
$hooked_blocks[] = 'my-plugin/related-posts';
}
return $hooked_blocks;
}, 10, 4 );
render.php
<?php
// Don't render if not on a single post
if ( ! is_singular( 'post' ) ) {
return '';
}
$current_post_id = get_the_ID();
$number_of_posts = $attributes['numberOfPosts'] ?? 3;
$show_thumbnails = $attributes['showThumbnails'] ?? true;
// Get related posts by category
$categories = get_the_category( $current_post_id );
if ( empty( $categories ) ) {
return '';
}
$related_posts = get_posts( [
'numberposts' => $number_of_posts,
'category__in' => wp_list_pluck( $categories, 'term_id' ),
'post__not_in' => [ $current_post_id ],
'orderby' => 'rand',
] );
if ( empty( $related_posts ) ) {
return '';
}
$wrapper_attributes = get_block_wrapper_attributes( [
'class' => 'related-posts',
] );
?>
<aside <?php echo $wrapper_attributes; ?>>
<h3><?php esc_html_e( 'Related Posts', 'my-plugin' ); ?></h3>
<ul class="related-posts__list">
<?php foreach ( $related_posts as $post ) : ?>
<li class="related-posts__item">
<?php if ( $show_thumbnails && has_post_thumbnail( $post ) ) : ?>
<a href="<?php echo esc_url( get_permalink( $post ) ); ?>" class="related-posts__thumbnail">
<?php echo get_the_post_thumbnail( $post, 'thumbnail' ); ?>
</a>
<?php endif; ?>
<a href="<?php echo esc_url( get_permalink( $post ) ); ?>" class="related-posts__title">
<?php echo esc_html( get_the_title( $post ) ); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</aside>
Hooks vs Templates vs Patterns
| Approach | Use When |
|---|---|
| Block Hooks | Automatic insertion, plugin-defined |
| Templates | User-customizable layouts |
| Patterns | Reusable content structures |
Block Hooks Are Best For:
- Plugin features that should appear automatically
- Default content that users can remove
- Cross-template consistent elements
Templates Are Better For:
- User-customizable layouts
- Theme-specific structures
- Full page designs
Debugging Hooks
Check which blocks are hooked:
add_action( 'init', function() {
$block_types = WP_Block_Type_Registry::get_instance()->get_all_registered();
foreach ( $block_types as $block_type ) {
if ( ! empty( $block_type->block_hooks ) ) {
error_log( sprintf(
'Block %s hooks: %s',
$block_type->name,
print_r( $block_type->block_hooks, true )
) );
}
}
} );
Best Practices
-
Be conservative: Only auto-insert where it makes sense.
-
Respect user choices: Don’t override user template modifications.
-
Use filters for conditions: Not everything should hook unconditionally.
-
Provide settings: Let users configure or disable hooked blocks.
-
Consider performance: Hooked blocks add to render time.
-
Document behavior: Tell users where blocks will appear.
-
Test all contexts: Verify hooks work in archives, singles, etc.
-
Handle empty states: Don’t render empty markup.