WordPress block.json Schema: apiVersion, Attributes, Supports, and Auto-Registration

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.