block.json

The metadata file both PHP and JavaScript read, and how register_block_type() turns a directory into a registered block.

Syntaxregister_block_type( string|WP_Block_Type $block_type, array $args = array() ): WP_Block_Type|false

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

Example · php
<?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.

Example · json
{
  "$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.

Example · php
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.

Example · php
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

  • Be the first to comment on this lesson.