Starting with WordPress 5.8, blocks can be registered entirely through a block.json metadata file rather than through JavaScript’s registerBlockType() alone. The file becomes the single source of truth for a block’s name, title, category, icon, supported attributes, editor/frontend scripts, styles, and feature support declarations. WordPress reads this file in both PHP (via register_block_type()) and JavaScript (via @wordpress/blocks), automatically handling script/style enqueueing, asset versioning, and attribute schema generation. Understanding the full block.json schema — including apiVersion, attributes with source bindings, supports, example for block previews, and variations — is the foundation of modern WordPress block development.
Problem: A block plugin uses a monolithic JavaScript file that registers all blocks. New blocks require adding registration code in three places — PHP, block registration JS, and webpack config. The goal is a self-contained per-block directory where adding a new block only requires creating the directory with a block.json.
Solution: Write a complete block.json for each block and use register_block_type( __DIR__ . '/blocks/my-block' ) to auto-register from the directory — WordPress reads the metadata and handles all enqueueing automatically.
// blocks/callout/block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"name": "my-plugin/callout",
"version": "1.0.0",
"title": "Callout",
"category": "text",
"icon": "megaphone",
"description": "A highlighted callout box with icon and message.",
"keywords": [ "callout", "notice", "alert" ],
"textdomain": "my-plugin",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p.callout__body",
"default": ""
},
"style": {
"type": "string",
"enum": [ "info", "warning", "success", "error" ],
"default": "info"
},
"align": {
"type": "string"
}
},
"supports": {
"align": [ "wide", "full" ],
"html": false,
"color": {
"text": true,
"background": true
},
"typography": {
"fontSize": true
},
"spacing": {
"padding": true
}
},
"example": {
"attributes": {
"content": "This is an important notice for your readers.",
"style": "info"
}
},
"editorScript": "file:./index.js",
"editorStyle": "file:./editor.css",
"style": "file:./style.css",
"render": "file:./render.php"
}
<?php
// Plugin main file — register all blocks from their directories
add_action( 'init', function () {
// Single block registration — reads block.json automatically
register_block_type( __DIR__ . '/blocks/callout' );
// Auto-register all blocks in the blocks/ directory
foreach ( glob( __DIR__ . '/blocks/*/block.json' ) as $block_json ) {
register_block_type( dirname( $block_json ) );
}
} );
// render.php — for server-side rendered blocks (the 'render' key in block.json)
// Variables available: $attributes (array), $content (string), $block (WP_Block)
?>
<div <?php echo get_block_wrapper_attributes( [
'class' => 'callout callout--' . esc_attr( $attributes['style'] ?? 'info' ),
] ); ?>>
<p class="callout__body"><?php echo wp_kses_post( $attributes['content'] ?? '' ); ?></p>
</div>
NOTE: The "render" key in block.json (added in WordPress 6.1) replaces the 'render_callback' PHP argument — the PHP file it points to is included with $attributes, $content, and $block in scope. For WordPress 5.8–6.0, use register_block_type( $dir, [ 'render_callback' => 'my_render_fn' ] ) instead. The "editorScript" value "file:./index.js" is a path relative to the block.json file — WordPress resolves it to an absolute URL automatically. Always run @wordpress/scripts (wp-scripts build) to compile the source files into the paths referenced in block.json.