block.json
The metadata file both PHP and JavaScript read, and how register_block_type() turns a directory into a registered block.
register_block_type( string|WP_Block_Type $block_type, array $args = array() ): WP_Block_Type|falseblock.json is the canonical description of a block: its name, title, attributes, supports and asset files. WordPress 5.8 made it the recommended registration path so the same metadata serves PHP, the editor's JavaScript and the /wp/v2/block-types REST endpoint, instead of being written out three times and drifting apart.
Registering it
Point the registration function at the directory containing the file:
// Shorthand — since WordPress 5.8 the first argument may be a path. register_block_type( __DIR__ . '/build/notice' ); // Longhand — what the call above delegates to. register_block_type_from_metadata( __DIR__ . '/build/notice' );
These do the same thing. Either accepts the folder or the block.json file itself, and both must run on init.
Assets
editorScript, script, viewScript (5.9), editorStyle and style each take a registered handle or a file: path relative to block.json. Writing "editorScript": "file:./index.js" is shorthand for calling wp_register_script() yourself and naming the handle in the field: WordPress generates the handle, reads the neighbouring index.asset.php for the dependency array and version, and enqueues it only where that field applies.
apiVersion
"apiVersion": 3 (WordPress 6.3) declares the block safe inside the editor's iframed canvas. Omitting the key means version 1, the behaviour from before useBlockProps.
Example
<?php
/**
* Plugin Name: Site Notice
* Requires at least: 6.8
* Requires PHP: 8.2
*/
add_action( 'init', 'sitenotice_register_blocks' );
function sitenotice_register_blocks(): void {
// One call per block. The argument is the folder holding block.json,
// which after a build lives under build/, not src/.
foreach ( array( 'notice', 'callout' ) as $block ) {
$dir = __DIR__ . '/build/' . $block;
// register_block_type() returns false if block.json is missing or invalid,
// which is the usual symptom of shipping without running the build.
if ( ! register_block_type( $dir ) ) {
_doing_it_wrong(
__FUNCTION__,
esc_html( "Could not register block from {$dir}" ),
'1.0.0'
);
}
}
}When to use it
- Adding a block to an existing plugin without duplicating its metadata in JS
- Making a block's settings visible to the REST API and to server-side code
- Wiring a compiled editor script and its dependencies without touching wp_enqueue_script
More examples
A complete block.json
The whole block described in one file. The $schema line is optional but gives editor autocomplete and validation. editorStyle points at index.css (styles imported from JS, editor only) while style points at style-index.css (loaded in both contexts) — the two filenames wp-scripts produces. Copy this as the starting point for any new block.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "sitenotice/notice",
"title": "Notice",
"category": "text",
"icon": "info",
"description": "A short highlighted message.",
"textdomain": "sitenotice",
"attributes": {
"message": { "type": "string", "source": "html", "selector": "p", "default": "" },
"level": { "type": "string", "default": "info" }
},
"supports": {
"color": { "background": true, "text": true },
"spacing": { "padding": true },
"anchor": true
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}Adding PHP-only settings alongside the metadata
Use the $args parameter when a block needs a closure or a function name that JSON cannot hold. Reach for this instead of abandoning block.json entirely — you keep the shared metadata and only add the PHP-side pieces. If the callback is a plain file of markup, prefer block.json's "render" key instead.
add_action( 'init', 'sitenotice_register_dynamic' );
function sitenotice_register_dynamic(): void {
// $args carries settings that cannot be expressed in JSON — anything
// holding a PHP callable. Everything else stays in block.json.
register_block_type(
__DIR__ . '/build/recent',
array(
'render_callback' => 'sitenotice_render_recent',
'uses_context' => array( 'postId' ),
)
);
}
function sitenotice_render_recent( array $attributes, string $content, WP_Block $block ): string {
return '<p>' . esc_html( (string) ( $attributes['label'] ?? '' ) ) . '</p>';
}One manifest for many blocks
Worth doing once a plugin ships more than a handful of blocks. The manifest is generated at build time and the collection call must come before the register_block_type() calls that it covers. On a plugin with two blocks the saving is not measurable; on one with twenty it is.
add_action( 'init', 'sitekit_register_collection' );
function sitekit_register_collection(): void {
// WordPress 6.7: registers a whole directory of blocks from a single
// generated manifest, so core reads one PHP file instead of stat-ing and
// json_decode-ing every block.json on every request.
wp_register_block_metadata_collection(
__DIR__ . '/build',
__DIR__ . '/build/blocks-manifest.php'
);
foreach ( array( 'notice', 'callout', 'recent', 'pricing' ) as $block ) {
register_block_type( __DIR__ . '/build/' . $block );
}
}
Discussion