2026年11月12日
2026年11月12日
WordPress Custom Block Plugin: create-block, block.json, render.php
This article is a localized edition of the Japanese original. Read the Japanese original
You can add blocks to the block editor (Gutenberg) with a plugin. The standard approach today is to describe the block in block.json, build the editor view in JavaScript (React), and produce the front-end HTML in a PHP file, render.php. A block whose output is generated in PHP is called a "dynamic block".
In this article we build a "CTA block" with a heading and a button. The steps and code were checked in November 2026 with WordPress 7.1, Node.js 22 and the latest @wordpress/create-block, all the way through building and displaying the block.
What you need
- Node.js and npm (to build the block's JavaScript; this article was checked with Node.js 22)
- A local WordPress environment (to install and test the plugin you build)
Note that WP-CLI's wp scaffold block command is deprecated (wp help scaffold block itself says to use the official @wordpress/create-block). This article uses create-block.
1. Scaffold the block with create-block
Run the following command in the wp-content/plugins folder.
cd wp-content/plugins
npx @wordpress/create-block@latest my-cta --variant dynamic --namespace myplugin --title "CTA Block"
| Option | Meaning |
|---|---|
my-cta |
The plugin folder name (also the second half of the block name) |
--variant dynamic |
Use the template for a dynamic block rendered in PHP (render.php) |
--namespace myplugin |
The first half of the block name. The block name becomes myplugin/my-cta |
--title |
The block's display name |
It installs the required packages and runs the first build automatically, producing the following files (main ones only).
my-cta/
├── my-cta.php # Main plugin file (registers the block)
├── package.json # Build commands
├── src/my-cta/
│ ├── block.json # Block metadata
│ ├── index.js # Block registration (JS)
│ ├── edit.js # Editor view and controls
│ ├── render.php # Front-end HTML
│ ├── style.scss # CSS for both the front end and the editor
│ ├── editor.scss # Editor-only CSS
│ └── view.js # JS that runs on the front end (delete it if unused)
└── build/ # Build output (this is what WordPress reads)
You edit the files in src. build is recreated on every build.
2. Register the block in the main plugin file
The generated my-cta.php (some headers and comments omitted) registers all blocks in the build folder like this:
<?php
/**
* Plugin Name: CTA Block
* Requires at least: 6.8
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
function myplugin_my_cta_block_init() {
wp_register_block_types_from_metadata_collection( __DIR__ . '/build', __DIR__ . '/build/blocks-manifest.php' );
}
add_action( 'init', 'myplugin_my_cta_block_init' );
wp_register_block_types_from_metadata_collection() was added in WordPress 6.8. It uses the blocks-manifest.php file generated at build time to register multiple blocks efficiently. With only one block, the older register_block_type( __DIR__ . '/build/my-cta' ); also works.
3. Define attributes and features in block.json
Rewrite src/my-cta/block.json as follows.
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "myplugin/my-cta",
"version": "0.1.0",
"title": "CTA Block",
"category": "widgets",
"icon": "megaphone",
"description": "A call-to-action block with a heading and a button",
"attributes": {
"heading": {
"type": "string",
"default": "Feel free to get in touch"
},
"buttonText": {
"type": "string",
"default": "Contact us"
},
"buttonUrl": {
"type": "string",
"default": ""
}
},
"supports": {
"html": false,
"align": [ "wide", "full" ],
"color": {
"background": true,
"text": true
},
"spacing": {
"padding": true
}
},
"textdomain": "my-cta",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"render": "file:./render.php"
}
What the main fields mean:
attributes: the values the block holds. Here there are three: the heading, the button text and the link URL. They are saved in the block comment in the post content (<!-- wp:myplugin/my-cta {...} /-->). Attributes left at their default are not saved, and thedefaultis used when the block is displayed.supports: adds core WordPress features to the block. Turning oncoloradds background and text color settings to the editor, andget_block_wrapper_attributes()(below) adds the classes and styles for the chosen colors automatically. You do not need to create color attributes yourself.render: the PHP file that produces the front-end HTML (added in WordPress 6.1). With it, you no longer need JavaScript to build the HTML to save.file:./entries such aseditorScript: these point to files inbuild. Use the file names after the build (index.js,style-index.cssand so on).
If you do not use view.js, delete src/my-cta/view.js and remove the viewScript line from block.json.
4. Produce the front-end HTML in render.php
Write src/my-cta/render.php as follows.
<?php
/**
* Prints the front-end HTML.
* $attributes (block attributes), $content (inner blocks) and $block (WP_Block) are available.
*/
$heading = $attributes['heading'] ?? '';
$button_text = $attributes['buttonText'] ?? '';
$button_url = esc_url( $attributes['buttonUrl'] ?? '' ); // Unsafe URLs become empty
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
<?php if ( '' !== $heading ) : ?>
<p class="wp-block-myplugin-my-cta__heading"><?php echo wp_kses_post( $heading ); ?></p>
<?php endif; ?>
<?php if ( '' !== $button_url && '' !== $button_text ) : ?>
<a class="wp-block-myplugin-my-cta__button" href="<?php echo $button_url; ?>">
<?php echo esc_html( $button_text ); ?>
</a>
<?php endif; ?>
</div>
get_block_wrapper_attributes()outputs the block class (wp-block-myplugin-my-cta) together with the color, spacing and alignment classes and styles added bysupports. Put it on the outer element.- The heading can have bold text or links added in the editor, so
wp_kses_post()keeps only allowed tags; the button text is escaped withesc_html(). - The URL goes through
esc_url()first. URLs such as ones starting withjavascript:become empty, and in that case the button is not shown.
For example, a block with a black background and white text produced the following HTML (whitespace tidied).
<div class="wp-block-myplugin-my-cta has-text-color has-white-color has-background has-black-background-color">
<p class="wp-block-myplugin-my-cta__heading">Feel free to get in touch</p>
<a class="wp-block-myplugin-my-cta__button" href="https://example.com/contact/">Contact us</a>
</div>
5. Build the editor view and controls in edit.js
Write src/my-cta/edit.js as follows. The heading is edited directly in the block, and the button text and URL are entered in the Settings sidebar on the right.
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls, RichText } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import './editor.scss';
export default function Edit( { attributes, setAttributes } ) {
const { heading, buttonText, buttonUrl } = attributes;
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Button settings', 'my-cta' ) }>
<TextControl
__next40pxDefaultSize
__nextHasNoMarginBottom
label={ __( 'Button text', 'my-cta' ) }
value={ buttonText }
onChange={ ( value ) => setAttributes( { buttonText: value } ) }
/>
<TextControl
__next40pxDefaultSize
__nextHasNoMarginBottom
type="url"
label={ __( 'Button link URL', 'my-cta' ) }
value={ buttonUrl }
onChange={ ( value ) => setAttributes( { buttonUrl: value } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...useBlockProps() }>
<RichText
tagName="p"
className="wp-block-myplugin-my-cta__heading"
value={ heading }
onChange={ ( value ) => setAttributes( { heading: value } ) }
placeholder={ __( 'Enter a heading', 'my-cta' ) }
/>
<span className="wp-block-myplugin-my-cta__button">
{ buttonText }
</span>
</div>
</>
);
}
useBlockProps()provides the attributes for the outer element in the editor. It is the counterpart of PHP'sget_block_wrapper_attributes(), so color and alignment settings show up in the editor too.- Whatever is inside
InspectorControlsappears in the Settings sidebar on the right. - At build time,
@wordpress/componentsis mapped to WordPress core'swp.components, so the build worked without installing it separately.
src/my-cta/index.js can stay as generated. Because this is a dynamic block, there is no save, and only the block comment (the attributes) is saved.
import { registerBlockType } from '@wordpress/blocks';
import './style.scss';
import Edit from './edit';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit: Edit,
} );
6. Write the CSS
src/my-cta/style.scss is loaded on both the front end and in the editor.
.wp-block-myplugin-my-cta {
padding: 2rem;
text-align: center;
&__heading {
margin: 0 0 1rem;
font-size: 1.25rem;
font-weight: bold;
}
&__button {
display: inline-block;
padding: 0.75em 1.5em;
border-radius: 4px;
background: #1d2327;
color: #fff;
text-decoration: none;
}
}
Put editor-only styling (such as an outline) in editor.scss.
7. Build and activate
cd my-cta
npm run build # Build src into build
npm start # During development: rebuild automatically every time you save a file
After building, activate "CTA Block" on the Plugins screen. In the post editor, add a block and search for "CTA Block" to find it.
You can create a ZIP file for distribution with npm run plugin-zip. On the production site you only need my-cta.php and the build folder; node_modules and src are not needed.
Check with WP-CLI
# Is the block registered?
wp eval 'var_dump( WP_Block_Type_Registry::get_instance()->is_registered( "myplugin/my-cta" ) );'
# Find posts and pages that use this block
wp post list --post_type=post,page --s='wp:myplugin/my-cta' --fields=ID,post_title
When the block does not show up
- The block is not in the inserter: check that you ran
npm run buildand thatbuild/my-cta/block.jsonexists. Editingsrcwithout building does not change anything in WordPress. - Nothing appears on the front end: check that
renderinblock.jsonisfile:./render.phpand thatbuild/my-cta/render.phpexists. Turn onWP_DEBUG_LOGand check the log for PHP errors. - Color settings are not applied: check that the outer element has
get_block_wrapper_attributes()(PHP) anduseBlockProps()(JS). - Page caching: dynamic block output is generated in PHP on every view, but with a caching plugin you may still see old HTML after fixing the block.
Related articles
- Basics of Gutenberg block development in WordPress
- Creating a custom Gutenberg block
- Building interactive blocks with the Interactivity API
- Adding block styles
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)