WordPress block.json Example: Create a Custom Block Without Theme Lock-In
Create a reusable custom block with block.json metadata, scoped assets, dynamic rendering, and frontend output escaping.
Published
April 17, 2026
Reading Time
3 min read
Updated
April 17, 2026

Implementation Notes
Extension points, code paths, and implementation choices that should survive contact with production.
Best For
WordPress developers, agencies, and technical teams building custom plugin or theme functionality with cleaner operational defaults.
Primary Topics
Editorial Focus
Build Pattern: Extension points, code paths, and implementation choices that should survive contact with production. Updated on April 17, 2026.
Full Report
Last reviewed: April 17, 2026
A modern WordPress custom block should not start as a random editor script pasted into a theme. The maintainable pattern is a small plugin with block.json metadata, registered assets, clear attributes, and a rendering strategy that matches the job. That is why searches for a WordPress block.json example keep growing as more sites move real editorial workflows into the block editor.
This guide shows the shape of a production-ready custom block plugin. It uses block.json as the source of truth, keeps the block independent from the active theme, and avoids the most common mistakes around asset loading and frontend output.
Who this guide is for
This article is for WordPress developers building reusable editor components, agencies replacing shortcodes, and teams that want custom editorial blocks without locking business logic into a theme.
Why block.json matters
WordPress supports block metadata through block.json. The metadata file describes the block name, title, scripts, styles, attributes, rendering behavior, editor support, and related details. Keeping this configuration in one file makes registration more predictable and helps WordPress load block assets only where they are needed.
Recommended plugin structure
vulnwp-notice-card/
vulnwp-notice-card.php
build/
block.json
index.js
render.php
style-index.css
In a real build process, the build directory is usually generated from a src directory by @wordpress/scripts or another bundler. The important part for this guide is the runtime contract: WordPress registers the built block metadata, not a loose collection of manually enqueued files.
Main plugin file
<?php
/**
* Plugin Name: VulnWP Notice Card Block
* Description: Example custom block registered from block.json.
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_action(
'init',
function () {
register_block_type( __DIR__ . '/build' );
}
);
The plugin does one thing: it registers the block from the metadata directory. This keeps block registration short and lets block.json define the important details.
Example block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "vulnwp/notice-card",
"version": "1.0.0",
"title": "Notice Card",
"category": "widgets",
"icon": "shield",
"description": "Displays a reusable editorial notice card.",
"textdomain": "vulnwp-notice-card",
"attributes": {
"message": {
"type": "string",
"default": "Review this deployment note before release."
}
},
"supports": {
"html": false
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css",
"render": "file:./render.php"
}
The name must be namespaced. Use a plugin or organization prefix and a stable block slug. Do not use a generic name that may collide with another plugin.
Editor script example
import { registerBlockType } from '@wordpress/blocks';
import { TextControl } from '@wordpress/components';
import { useBlockProps } from '@wordpress/block-editor';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps( {
className: 'vulnwp-notice-card',
} );
return (
<div { ...blockProps }>
<strong>Notice Card</strong>
<TextControl
label="Message"
value={ attributes.message }
onChange={ ( message ) => setAttributes( { message } ) }
/>
</div>
);
},
} );
This editor script keeps the block simple: one string attribute and one editor control. Complex blocks should still start with a small attribute model. If every content decision becomes deeply nested JSON, future migrations become harder.
Dynamic render example
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$message = isset( $attributes['message'] )
? sanitize_text_field( $attributes['message'] )
: '';
if ( '' === $message ) {
return;
}
?>
<div <?php echo get_block_wrapper_attributes( array( 'class' => 'vulnwp-notice-card' ) ); ?>>
<strong><?php esc_html_e( 'Deployment note', 'vulnwp-notice-card' ); ?></strong>
<p><?php echo esc_html( $message ); ?></p>
</div>
This example uses dynamic rendering because the final HTML is controlled by PHP. Dynamic rendering is useful when output should stay centrally controlled, depend on server-side data, or evolve without opening every post in the editor.
When to use static rendering instead
Static blocks save the final markup into post content. They can be ideal for simple presentational components that do not need server-side data. Dynamic blocks are better when output depends on plugin settings, database data, permissions, API responses, or markup you may need to change later across many posts.
Production checklist
- Register the block from a plugin unless the block is truly theme-only.
- Use a namespaced block name such as
vendor/block-name. - Declare attributes explicitly in
block.json. - Use
supports.html: falsewhen raw HTML editing would create risk or confusion. - Escape frontend output in the render callback.
- Keep frontend styles scoped to the block class.
- Test the editor, frontend, REST response, and deactivation behavior.
Common mistakes
- Registering blocks in the theme by default. Reusable editorial blocks should survive theme changes.
- Using vague block names. Generic names increase collision risk and make debugging harder.
- Loading scripts globally. Block metadata lets WordPress load block assets more selectively.
- Skipping output escaping. Attribute data still needs context-aware escaping on the frontend.
- Overbuilding the first version. Start with the smallest block contract that solves the editorial need.
Related reading
If you are replacing shortcodes with blocks, read our safe shortcode example first. It helps identify which shortcode attributes should become block attributes and which output should remain server-side.


