with one click
add-block
Add a new Gutenberg block to an existing plugin or theme.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Add a new Gutenberg block to an existing plugin or theme.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Add a WordPress AJAX handler to a plugin, with nonce verification, input sanitisation, and JSON response.
Register a block pattern in a theme or plugin, using the WordPress 6.0+ file-based pattern system.
Register a WP-CLI command in a plugin using a command class, following Dekode conventions.
Add a WordPress action or filter hook to a plugin or theme.
Scaffold a new must-use plugin in packages/mu-plugins/ - either a simple utility or a service-class integration.
Register a custom post type (and optionally a taxonomy) in a theme or plugin, following Dekode conventions.
| name | add-block |
| description | Add a new Gutenberg block to an existing plugin or theme. |
Register a new block in an existing plugin or theme following WordPress block development conventions.
Ask the user: "What will this block do?" — a brief description of what it displays or enables in the editor.
Based on the description, suggest 2–3 name options (kebab-case), for example:
Based on your description, here are some suggestions:
# Block name 1 featured-card2 promo-card3 highlight-cardWhich would you like to use, or do you have your own preference?
Once the name is confirmed, apply the find-plugin skill to determine whether:
Only proceed to the steps below if Core and existing plugins do not cover the need, or if the user has explicitly agreed to build a custom block after reviewing the recommendation.
| Input | Required | Notes |
|---|---|---|
| Target package | Yes | Which theme or plugin should own this block? Scan packages/ and list options. |
| Block vendor prefix | Yes | The namespace prefix used in block.json name, e.g. acme → acme/hero-banner. Check sibling block.json files in the plugin to confirm. |
| Block category | No | One of text, media, design, widgets, theme, embed. Default: design. |
| Is it dynamic (server-rendered)? | No | If yes, save() returns null and a PHP render callback is provided. Defaults to yes for server-rendered projects. |
| Attributes | No | Ask what data the block needs to store (text, image URL, boolean toggle, etc.). |
| Needs frontend JS? | No | Does the block need a view.js for frontend interactivity? Defaults to no. |
Look for an existing block registration pattern in the target package. Typical location:
src/blocks/<block-name>/ (or src/<block-name>/ depending on plugin structure)
block.json
block.php (dynamic: registration + render via namespaced functions)
edit.js
index.js
save.js (omit if dynamic)
style.scss (front-end styles)
editor.scss (editor-only styles)
view.js (only if frontend interactivity is needed)
Read the plugin's main PHP file to confirm the PHP namespace and how blocks are registered/loaded.
block.json{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "<vendor>/<block-name>",
"version": "1.0.0",
"title": "<Block Title>",
"category": "<category>",
"description": "",
"supports": {
"html": false,
"align": ["wide", "full"]
},
"attributes": {},
"textdomain": "<plugin-slug>",
"editorScript": "file:index.js",
"editorStyle": "file:index.css",
"style": "file:style-index.css"
}
"render" — the render callback is registered in block.php."viewScript": "file:view.js" (omit otherwise).block.php (dynamic blocks only)<?php
/**
* Block: <Block Title>
*
* @package <PluginNamespace>
*/
declare( strict_types=1 );
namespace <PluginNamespace>\Blocks\<BlockNamePascal>;
/**
* Register block.
*/
function register_block(): void {
register_block_type_from_metadata(
__DIR__,
[
'render_callback' => __NAMESPACE__ . '\\render_block',
]
);
}
add_action( 'init', __NAMESPACE__ . '\\register_block' );
/**
* Render block.
*
* @param array<string, mixed> $attributes Block attributes.
* @param string $content Block inner content.
* @return string
*/
function render_block( array $attributes, string $content ): string {
$wrapper_attributes = get_block_wrapper_attributes();
ob_start();
?>
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<?php // Block output here ?>
</div>
<?php
return ob_get_clean() ?: '';
}
namespace to the plugin's existing namespace (e.g. ProjectBase\BlockLibrary\Blocks\HeroBanner).get_block_wrapper_attributes() for the wrapper element.ob_start()/ob_get_clean() for template rendering.esc_html(), esc_attr(), esc_url(), wp_kses_post().index.js/**
* WordPress dependencies
*/
import { registerBlockType } from '@wordpress/blocks';
/**
* Internal dependencies
*/
import metadata from './block.json';
import Edit from './edit';
import save from './save'; // omit for dynamic blocks
registerBlockType( metadata.name, {
edit: Edit,
save, // omit for dynamic blocks
} );
edit.js/**
* WordPress dependencies
*/
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
/**
* @param {Object} props Block props.
* @return {JSX.Element} Block editor UI.
*/
export default function Edit( { attributes, setAttributes } ) {
const { title } = attributes;
const blockProps = useBlockProps();
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Settings', '<text-domain>' ) }>
{ /* Add controls here */ }
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<RichText
tagName="h2"
value={ title }
onChange={ ( value ) => setAttributes( { title: value } ) }
placeholder={ __( 'Add a title…', '<text-domain>' ) }
/>
</div>
</>
);
}
save.js (static blocks only)/**
* WordPress dependencies
*/
import { useBlockProps, RichText } from '@wordpress/block-editor';
/**
* @param {Object} props Block props.
* @return {JSX.Element} Saved block markup.
*/
export default function save( { attributes } ) {
const { title } = attributes;
return (
<div { ...useBlockProps.save() }>
<RichText.Content tagName="h2" value={ title } />
</div>
);
}
view.js (only if frontend interactivity is needed)// Frontend behaviour for <Block Title> block
document.querySelectorAll( '.wp-block-<vendor>-<block-name>' ).forEach( ( block ) => {
// Initialise block behaviour
} );
Open the plugin's main PHP file and confirm it loads blocks via glob:
foreach ( glob( __DIR__ . '/build/*/block.php' ) as $block ) {
require_once $block;
}
If the glob pattern is missing, add it. If the plugin uses a different registration pattern (e.g. a BlockRegistrar class), follow that pattern instead.
cd packages/plugins/<plugin-slug>
npm run build # or from root: npm run build
npm run lint:js
npm run lint:css
composer lint
Confirm the block appears in build/<block-name>/ after the build.
import statements grouped: WordPress core → internal. The @wordpress/dependency-group ESLint rule is enforcedget_block_wrapper_attributes() in PHP render — this enables block spacing, align, and custom className support@wordpress/* packages or native DOM APIsWhen done, the following files exist under src/<block-name>/:
block.json — block metadatablock.php — PHP server-side render + registration (dynamic blocks)index.js — block registration entry pointedit.js — block editor componentsave.js — static save output (static blocks only)view.js — (optional) frontend scriptAfter npm run build, the block appears in build/<block-name>/ and is loadable in the WordPress editor. Lint passes with no errors.