gutenberg-block
Skill1 starsChanged 4 months agoMITView on GitHub
Generate a custom Gutenberg block. Use this when the user asks to create a block, add a Gutenberg block to a plugin or theme, or scaffold block.json with edit, save, or render sources. Ask dynamic vs static first. Produces block.json, edit.js, save.js or render.php, style.scss, and a view script when needed.
- Installs packages
In this repository5 other files it gives its agents.
- db-migrationSkill
- plugin-scaffoldSkill
- security-auditSkill
- theme-initSkill
- wp-debugSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 1 is distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 10
Create a custom block registered with block.json and placed inside a plugin or theme blocks/ directory.
- Distinctive: line 20
If the block type (dynamic or static) is not given, ask the user which to use.
- Very specific: line 26
Write block.json, edit.js, the right save target (save.js for static or render.php for dynamic), and style.scss.
- Very specific: line 292
Do not use apiVersion 2. New blocks must use apiVersion 3.
- Very specific: line 293
Do not mix dynamic and static output.
- Very specific: line 294
Do not hand-write build/ output.
- Very specific: line 295
Do not echo attributes directly in render.php.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Generate a custom Gutenberg block
Create a custom block registered with block.json and placed inside a plugin or theme blocks/ directory. The block works with @wordpress/scripts version 30.x for the build step.
Inputs
$ARGUMENTS is the block name and an optional type: testimonial, testimonial dynamic, pricing static.
Block names are lowercase, hyphen-separated, and are prefixed with the plugin or theme namespace to produce the final block name, for example acme/testimonial.
Workflow
- If the block type (dynamic or static) is not given, ask the user which to use. Explain the difference in one line:
- Static: HTML is saved into post content at publish time. Fast, no PHP at render. Cannot reflect later data changes.
- Dynamic: HTML is produced at render time by a PHP callback. Slower but always reflects current data. Required for query-based or user-specific output.
- Decide the namespace. If inside a plugin, use the plugin slug. If inside a theme, use the theme slug. Ask if ambiguous.
- Choose a directory:
blocks/<block-name>/inside the plugin or theme. - Announce each file you are about to write.
- Write
block.json,edit.js, the right save target (save.jsfor static orrender.phpfor dynamic), andstyle.scss. - Print the build command and the registration snippet.
Files for a static block
blocks/testimonial/block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "acme/testimonial",
"title": "Testimonial",
"category": "widgets",
"icon": "format-quote",
"description": "A customer testimonial with a quote and an attribution.",
"textdomain": "acme-forms",
"attributes": {
"quote": { "type": "string", "default": "" },
"attribution": { "type": "string", "default": "" }
},
"supports": {
"html": false,
"anchor": true,
"align": ["wide", "full"]
},
"editorScript": "file:./index.js",
"style": "file:./style-index.css"
}Declare editorStyle in block.json only if the block also imports an editor-only stylesheet. Adding editorStyle without a matching build output leaves WordPress enqueuing a 404 in the editor.
blocks/testimonial/edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function Edit( { attributes, setAttributes } ) {
const { quote, attribution } = attributes;
const blockProps = useBlockProps();
return (
<blockquote { ...blockProps }>
<RichText
tagName="p"
value={ quote }
onChange={ ( value ) => setAttributes( { quote: value } ) }
placeholder={ __( 'Quote text...', 'acme-forms' ) }
/>
<RichText
tagName="cite"
value={ attribution }
onChange={ ( value ) => setAttributes( { attribution: value } ) }
placeholder={ __( 'Attribution', 'acme-forms' ) }
/>
</blockquote>
);
}blocks/testimonial/save.js
import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { quote, attribution } = attributes;
const blockProps = useBlockProps.save();
return (
<blockquote { ...blockProps }>
<RichText.Content tagName="p" value={ quote } />
<RichText.Content tagName="cite" value={ attribution } />
</blockquote>
);
}blocks/testimonial/index.js
import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import save from './save';
import './style.scss';
registerBlockType( metadata.name, {
edit: Edit,
save,
} );blocks/testimonial/style.scss
.wp-block-acme-testimonial {
border-left: 4px solid var(--wp--preset--color--accent, #2563eb);
padding: 1rem 1.5rem;
margin: 1.5rem 0;
p {
font-size: 1.125rem;
line-height: 1.5;
margin: 0 0 0.5rem;
}
cite {
font-style: normal;
font-weight: 600;
}
}Files for a dynamic block
The difference is that save returns null and the block declares render in block.json.
blocks/latest-post/block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "acme/latest-post",
"title": "Latest Post",
"category": "widgets",
"icon": "admin-post",
"description": "Renders the most recent post from the site at view time.",
"textdomain": "acme-forms",
"attributes": {
"postType": { "type": "string", "default": "post" }
},
"supports": { "html": false, "anchor": true },
"editorScript": "file:./index.js",
"style": "file:./style-index.css",
"render": "file:./render.php"
}blocks/latest-post/index.js
import { registerBlockType } from '@wordpress/blocks';
import metadata from './block.json';
import Edit from './edit';
import './style.scss';
registerBlockType( metadata.name, {
edit: Edit,
save: () => null,
} );blocks/latest-post/edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';
export default function Edit() {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<ServerSideRender
block="acme/latest-post"
EmptyResponsePlaceholder={ () => <p>{ __( 'No posts found.', 'acme-forms' ) }</p> }
/>
</div>
);
}blocks/latest-post/render.php
<?php
/**
* Dynamic render for acme/latest-post.
*
* @var array $attributes Block attributes.
* @var string $content Inner content.
* @var WP_Block $block Block instance.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$post_type = isset( $attributes['postType'] ) ? sanitize_key( $attributes['postType'] ) : 'post';
$query = new WP_Query( array(
'post_type' => $post_type,
'posts_per_page' => 1,
'no_found_rows' => true,
) );
if ( ! $query->have_posts() ) {
return;
}
$query->the_post();
$title = get_the_title();
$link = get_permalink();
wp_reset_postdata();
$wrapper = get_block_wrapper_attributes();
printf(
'<div %1$s><a href="%2$s">%3$s</a></div>',
$wrapper,
esc_url( $link ),
esc_html( $title )
);Registration snippet
The plugin or theme must register the block directory from PHP. Add this once and point it at the build output:
add_action( 'init', 'acme_forms_register_blocks' );
function acme_forms_register_blocks() {
register_block_type( __DIR__ . '/build/testimonial' );
register_block_type( __DIR__ . '/build/latest-post' );
}Build
Install and build with @wordpress/scripts pinned to a known-good version:
npm init -y
npm install --save-dev @wordpress/scripts@30.26.2
npx wp-scripts build --webpack-src-dir=blocks --output-path=buildFor local development with watch mode:
npx wp-scripts start --webpack-src-dir=blocks --output-path=buildExample
User runs /wordpress-devkit:gutenberg-block testimonial static.
The skill writes these files:
blocks/testimonial/
block.json
index.js
edit.js
save.js
style.scssIt then prints the registration snippet and the two build commands.
Do not
- Do not use
apiVersion 2. New blocks must useapiVersion 3. - Do not mix dynamic and static output. A dynamic block's
savereturnsnull. A static block has norender.php. - Do not hand-write
build/output. Let@wordpress/scriptsproduce it. - Do not echo attributes directly in
render.php. Always escape withesc_html,esc_attr,esc_url, orwp_kses_postbased on the context.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

