Use when working with WordPress Gutenberg blocks and themes
This skill guides development of WordPress Gutenberg blocks, Full Site Editing (FSE) themes, block variations, and PHP server-side rendered dynamic blocks.
register_block_type)Before writing any code, detect the build setup:
# Check for @wordpress/scripts (most common)
cat package.json | grep -E '"@wordpress/scripts|wp-scripts"'
# Check for custom webpack
ls webpack.config.js webpack.config.ts 2>/dev/null
# Check for Vite
ls vite.config.js vite.config.ts 2>/dev/nullnpm run start # Dev mode with hot reload
npm run build # Production build
npm run lint:js # ESLint with @wordpress rules
npm run lint:css # StylelintRead the existing config before adding blocks — source paths and output directories may differ from WordPress defaults.
Every block requires a block.json manifest. Create blocks following this layout:
src/blocks/my-block/
├── block.json # Block metadata (required)
├── index.js # Registration entry point
├── edit.js # Editor component
├── save.js # Frontend save function (static blocks)
├── render.php # Server-side render (dynamic blocks)
├── editor.scss # Editor-only styles
└── style.scss # Frontend + editor styles{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/my-block",
"version": "1.0.0",
"title": "My Block",
"category": "text",
"description": "A custom block.",
"supports": {
"html": false,
"color": { "background": true, "text": true },
"spacing": { "margin": true, "padding": true },
"typography": { "fontSize": true }
},
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p",
"default": ""
}
},
"editorScript": "file:./index.js",
"editorStyle": "file:./editor.css",
"style": "file:./style-index.css",
"render": "file:./render.php"
}import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import Edit from './edit';
import Save from './save';
import metadata from './block.json';
registerBlockType( metadata.name, {
edit: Edit,
save: Save,
} );import { useBlockProps, RichText } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
return (
<div { ...blockProps }>
<RichText
tagName="p"
value={ attributes.content }
onChange={ ( content ) => setAttributes( { content } ) }
placeholder={ __( 'Enter content…', 'my-plugin' ) }
aria-label={ __( 'Block content', 'my-plugin' ) }
/>
</div>
);
}import { useBlockProps, RichText } from '@wordpress/block-editor';
export default function Save( { attributes } ) {
const blockProps = useBlockProps.save();
return (
<div { ...blockProps }>
<RichText.Content tagName="p" value={ attributes.content } />
</div>
);
}For blocks whose output depends on runtime data (queries, user state, etc.), use render.php instead of save.js.
<?php
/**
* Dynamic block render callback.
*
* @param array $attributes Block attributes.
* @param string $content Inner blocks content.
* @param WP_Block $block Block instance.
*/
$wrapper_attributes = get_block_wrapper_attributes( [
'class' => 'my-block',
] );
?>
<div <?php echo $wrapper_attributes; ?>>
<?php echo esc_html( $attributes['content'] ?? '' ); ?>
</div>function my_plugin_register_blocks(): void {
register_block_type( __DIR__ . '/build/blocks/my-block' );
}
add_action( 'init', 'my_plugin_register_blocks' );Prefer register_block_type( path_to_block_json_dir ) over manual registration — it reads all metadata from block.json automatically.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"color": {
"palette": [
{ "slug": "primary", "color": "#0073aa", "name": "Primary" },
{ "slug": "secondary", "color": "#23282d", "name": "Secondary" }
]
},
"typography": {
"fontFamilies": [
{
"name": "System Font",
"slug": "system-font",
"fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
}
],
"fontSizes": [
{ "slug": "small", "size": "0.875rem", "name": "Small" },
{ "slug": "normal", "size": "1rem", "name": "Normal" },
{ "slug": "large", "size": "1.5rem", "name": "Large" }
]
},
"spacing": {
"spacingScale": { "steps": 7 }
},
"layout": {
"contentSize": "800px",
"wideSize": "1200px"
}
},
"styles": {
"color": {
"background": "var(--wp--preset--color--white)",
"text": "var(--wp--preset--color--secondary)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--system-font)",
"fontSize": "var(--wp--preset--font-size--normal)"
}
}
}theme-name/
├── theme.json # Global settings & styles
├── style.css # Theme header
├── functions.php # Theme setup
├── templates/ # Full page templates
│ ├── index.html
│ ├── single.html
│ ├── archive.html
│ ├── 404.html
│ └── page.html
├── parts/ # Reusable template parts
│ ├── header.html
│ └── footer.html
└── patterns/ # Block patterns (PHP or HTML)
└── hero.phpTemplates are block markup HTML files. Always use <!-- wp:... --> comments:
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"tagName":"main","layout":{"type":"constrained"}} -->
<main class="wp-block-group">
<!-- wp:post-content /-->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /--><?php
/**
* Hero pattern.
*
* @package My Theme
*/
return [
'title' => __( 'Hero Section', 'my-theme' ),
'categories' => [ 'featured' ],
'content' => '<!-- wp:cover {"minHeight":400} -->
<div class="wp-block-cover">
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading">' . __( 'Welcome', 'my-theme' ) . '</h1>
<!-- /wp:heading -->
</div>
<!-- /wp:cover -->',
];Register the pattern directory in functions.php:
add_action( 'after_setup_theme', function(): void {
register_block_pattern_category( 'featured', [
'label' => __( 'Featured', 'my-theme' ),
] );
} );import { registerBlockVariation } from '@wordpress/blocks';
registerBlockVariation( 'core/group', {
name: 'my-plugin/card',
title: 'Card',
description: 'A group styled as a card.',
attributes: {
className: 'is-style-card',
layout: { type: 'constrained' },
},
isDefault: false,
scope: [ 'inserter', 'transform' ],
} );import { addFilter } from '@wordpress/hooks';
import { createHigherOrderComponent } from '@wordpress/compose';
// Add a custom class option to the Inspector Controls
const withCustomSupport = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
if ( props.name !== 'core/paragraph' ) {
return <BlockEdit { ...props } />;
}
return <BlockEdit { ...props } />;
};
}, 'withCustomSupport' );
addFilter(
'editor.BlockEdit',
'my-plugin/with-custom-support',
withCustomSupport
);@wordpress/eslint-plugin rules@wordpress/* packages, not external equivalents (e.g., use @wordpress/element, not react directly)use__(), _n(), _x() from @wordpress/i18n for all user-facing stringsnpm run lint:jssanitize_text_field(), absint(), etc.esc_html(), esc_attr(), wp_kses_post()composer lint / phpcsEvery block must meet WCAG 2.1 AA:
<nav>, <article>, <button>, etc.)aria-label when element purpose isn't conveyed by visible text<img> elements must have descriptive alt attributes<label>aria-live for dynamic content updates// Good: accessible button
<Button
onClick={ handleClick }
aria-label={ __( 'Remove item', 'my-plugin' ) }
icon={ closeIcon }
/>
// Bad: icon-only button with no label
<button onClick={ handleClick }>✕</button>When the saved markup doesn't match what save() returns:
// Check the block's serialized output
wp.blocks.serialize( wp.data.select('core/block-editor').getBlocks() )Options:
save() to match current markuprender.php to avoid save validationEnsure the component is inside <InspectorControls> from @wordpress/block-editor:
import { InspectorControls, useBlockProps } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';# Check block is registered
wp eval "var_dump( WP_Block_Type_Registry::get_instance()->get_all_registered() );" | grep my-pluginhttps://developer.wordpress.org/block-editor/reference-guides/block-api/https://schemas.wp.org/trunk/theme.jsonhttps://schemas.wp.org/trunk/block.json@wordpress/components: https://wordpress.github.io/gutenberg/?path=/docs/componentshttps://developer.wordpress.org/coding-standards/https://www.w3.org/TR/WCAG21/Prefer retrieval-led reasoning
[Documentation Index]|root: /Users/stephenfeather/.claude/skills/agent-gutenberg|references/guides:{block-api-version-2-useblockprops.md,choose-your-adventure.md,wordpress-data-api-useselect-usedispatch.md,extend-a-core-block.md,integrating-third-party-js-libraries-in-blocks.md,block-spacing-theme-json-spacingsizes.md,wp-html-tag-processor-php-block-markup.md,including-frontend-javascript-with-a-block.md,interactivity-api-getting-started.md,modifying-the-markup-of-a-core-block.md,block-styles-pitfalls-and-addfilter-alternative.md,inspector-controls-toolspanel-toolspanelitem.md,using-wordpress-packages-on-the-frontend.md}|references/reference:{05-custom-post-types.md}|references/reference/01-Fundamentals:{anatomy-of-a-block-toolbar-sidebar-states.md,block-editor-anatomy-overview.md}|references/reference/02-Themes:{block-based-templates.md,block-template-parts.md,fonts.md,navigation.md,styles.md,theme-json.md}|references/reference/03-Blocks:{block-extensions.md,block-locking.md,block-styles.md,block-supports.md,block-transforms.md,block-deprecations-and-attribute-migrations.md,block-variations.md,custom-blocks.md,inner-blocks.md,unregister-block.md}|references/reference/04-Patterns:{block-bindings-api.md,block-patterns-overview.md,synced-pattern-overrides.md,synced-patterns.md}|references/training/Block-Based-Themes:{01-overview.md,index.md}|references/training/Blocks:{01-overview.md,02-cta-lesson.md,03-styles.md,04-patterns.md,05-variations.md,06-inner-blocks.md,07-rich-text-formats.md,08-slot-fill.md,09-build-your-own.md,10-Using the Block Scaffold command.md,index.md}
1721217
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.