toolchains/php/frameworks/wordpress/block-editor/SKILL.md
Modern WordPress block development and Full Site Editing with theme.json, block themes, and custom blocks for WordPress 6.7+
npx skillsauth add bobmatnyc/claude-mpm-skills wordpress-block-editor-fseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Full Site Editing (FSE) is production-ready (since WP 6.2) and treats everything as blocks—headers, footers, templates, not just content. Block themes use HTML templates + theme.json instead of PHP files + style.css.
Key Components:
When to Use: ✅ New themes, consistent design systems, non-technical user customization ❌ Complex server logic, team unfamiliar with blocks, heavy PHP dependencies
| Block Themes | Classic Themes | |-------------|----------------| | HTML files with blocks | PHP files with template tags | | theme.json + CSS | functions.php + style.css | | Site Editor (visual) | Customizer (settings) | | User edits templates | Limited customization |
theme.json v3 (WP 6.7) provides centralized design control. WordPress auto-generates CSS custom properties.
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"appearanceTools": true,
"useRootPaddingAwareAlignments": true,
"layout": {
"contentSize": "800px",
"wideSize": "1200px"
},
"color": {
"palette": [
{ "slug": "primary", "color": "#0073aa", "name": "Primary" },
{ "slug": "secondary", "color": "#005177", "name": "Secondary" },
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "contrast", "color": "#000000", "name": "Contrast" }
],
"defaultPalette": false,
"defaultGradients": false
},
"typography": {
"fontFamilies": [
{
"fontFamily": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
"slug": "system",
"name": "System Font"
}
],
"fontSizes": [
{ "slug": "small", "size": "0.875rem", "name": "Small" },
{ "slug": "medium", "size": "1rem", "name": "Medium" },
{
"slug": "large",
"size": "1.5rem",
"name": "Large",
"fluid": { "min": "1.25rem", "max": "1.5rem" }
}
],
"fontWeight": true,
"lineHeight": true
},
"spacing": {
"units": ["px", "em", "rem", "vh", "vw", "%"],
"padding": true,
"margin": true,
"spacingSizes": [
{ "slug": "30", "size": "0.5rem", "name": "XS" },
{ "slug": "40", "size": "1rem", "name": "S" },
{ "slug": "50", "size": "1.5rem", "name": "M" },
{ "slug": "60", "size": "2rem", "name": "L" }
]
},
"border": { "radius": true, "color": true, "width": true }
},
"styles": {
"color": {
"background": "var(--wp--preset--color--base)",
"text": "var(--wp--preset--color--contrast)"
},
"typography": {
"fontFamily": "var(--wp--preset--font-family--system)",
"fontSize": "var(--wp--preset--font-size--medium)",
"lineHeight": "1.6"
},
"elements": {
"link": {
"color": { "text": "var(--wp--preset--color--primary)" },
":hover": {
"color": { "text": "var(--wp--preset--color--secondary)" }
}
},
"h1": {
"typography": {
"fontSize": "var(--wp--preset--font-size--large)",
"fontWeight": "700"
}
},
"button": {
"color": {
"background": "var(--wp--preset--color--primary)",
"text": "var(--wp--preset--color--base)"
},
"border": { "radius": "4px" },
":hover": {
"color": { "background": "var(--wp--preset--color--secondary)" }
}
}
},
"blocks": {
"core/quote": {
"border": {
"width": "0 0 0 4px",
"color": "var(--wp--preset--color--primary)"
},
"spacing": { "padding": { "left": "var(--wp--preset--spacing--60)" } }
}
}
},
"customTemplates": [
{
"name": "page-wide",
"title": "Full Width Page",
"postTypes": ["page"]
}
]
}
var(--wp--preset--color--primary)var(--wp--preset--font-family--system)var(--wp--preset--font-size--large)var(--wp--preset--spacing--50)Font sizes with fluid: { min, max } auto-scale using clamp():
{
"slug": "large",
"size": "1.5rem",
"fluid": { "min": "1.25rem", "max": "1.5rem" }
}
my-block-theme/
├── style.css # Theme metadata (REQUIRED)
├── theme.json # Settings/styles (REQUIRED)
├── templates/
│ ├── index.html # Fallback (REQUIRED)
│ ├── single.html
│ ├── page.html
│ └── archive.html
├── parts/
│ ├── header.html
│ └── footer.html
├── patterns/ # Block patterns
│ └── hero.php
└── functions.php # Optional setup
/*
Theme Name: My Block Theme
Requires at least: 6.4
Requires PHP: 8.1
Version: 1.0.0
*/
templates/single.html:
<!-- wp:template-part {"slug":"header","tagName":"header"} /-->
<!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group">
<!-- wp:post-title {"level":1} /-->
<!-- wp:post-featured-image /-->
<!-- wp:post-content /-->
<!-- wp:post-date /-->
</div>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer","tagName":"footer"} /-->
templates/index.html (with query loop):
<!-- wp:template-part {"slug":"header"} /-->
<!-- wp:group {"tagName":"main"} -->
<main class="wp-block-group">
<!-- wp:query {"queryId":1,"query":{"perPage":10,"postType":"post"}} -->
<div class="wp-block-query">
<!-- wp:post-template {"layout":{"type":"grid","columnCount":3}} -->
<!-- wp:post-featured-image {"isLink":true} /-->
<!-- wp:post-title {"isLink":true} /-->
<!-- wp:post-excerpt /-->
<!-- /wp:post-template -->
<!-- wp:query-pagination -->
<!-- wp:query-pagination-previous /-->
<!-- wp:query-pagination-numbers /-->
<!-- wp:query-pagination-next /-->
<!-- /wp:query-pagination -->
</div>
<!-- /wp:query -->
</main>
<!-- /wp:group -->
<!-- wp:template-part {"slug":"footer"} /-->
parts/header.html:
<!-- wp:group {"layout":{"type":"flex","justifyContent":"space-between"}} -->
<div class="wp-block-group">
<!-- wp:site-logo {"width":60} /-->
<!-- wp:navigation /-->
</div>
<!-- /wp:group -->
patterns/hero.php:
<?php
/**
* Title: Hero Section
* Slug: my-theme/hero
* Categories: featured
*/
?>
<!-- wp:cover {"url":"<?php echo esc_url(get_template_directory_uri()); ?>/assets/images/hero.jpg","dimRatio":50,"minHeight":500,"align":"full"} -->
<div class="wp-block-cover alignfull">
<div class="wp-block-cover__inner-container">
<!-- wp:heading {"textAlign":"center","level":1,"fontSize":"xx-large"} -->
<h1>Welcome to Our Site</h1>
<!-- /wp:heading -->
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
<div class="wp-block-buttons">
<!-- wp:button -->
<div class="wp-block-button"><a class="wp-block-button__link">Get Started</a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
</div>
</div>
<!-- /wp:cover -->
Register pattern categories:
add_action('init', 'register_pattern_categories');
function register_pattern_categories() {
register_block_pattern_category('hero', [
'label' => __('Hero Sections', 'my-theme')
]);
register_block_pattern_category('cta', [
'label' => __('Call to Action', 'my-theme')
]);
}
blocks/testimonial/block.json:
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-theme/testimonial",
"title": "Testimonial",
"category": "widgets",
"icon": "format-quote",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": ".testimonial-content"
},
"author": { "type": "string", "default": "" },
"role": { "type": "string", "default": "" },
"rating": { "type": "number", "default": 5 }
},
"supports": {
"html": false,
"align": ["wide", "full"],
"color": { "background": true, "text": true },
"spacing": { "padding": true, "margin": true }
},
"render": "file:./render.php"
}
Different ways to extract data from HTML:
"attributes": {
"title": {
"type": "string",
"source": "html",
"selector": "h2"
},
"linkUrl": {
"type": "string",
"source": "attribute",
"selector": "a",
"attribute": "href"
},
"isActive": {
"type": "boolean",
"default": false
},
"items": {
"type": "array",
"source": "query",
"selector": ".item",
"query": {
"text": { "type": "string", "source": "text" }
}
}
}
blocks/testimonial/render.php:
<?php
$content = $attributes['content'] ?? '';
$author = $attributes['author'] ?? '';
$role = $attributes['role'] ?? '';
$rating = absint($attributes['rating'] ?? 5);
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'testimonial-block',
]);
?>
<div <?php echo $wrapper_attributes; ?>>
<blockquote class="testimonial-content">
<?php echo wp_kses_post($content); ?>
</blockquote>
<?php if ($rating > 0) : ?>
<div class="testimonial-rating">
<?php for ($i = 1; $i <= 5; $i++) : ?>
<span class="star <?php echo $i <= $rating ? 'filled' : 'empty'; ?>">
<?php echo $i <= $rating ? '★' : '☆'; ?>
</span>
<?php endfor; ?>
</div>
<?php endif; ?>
<?php if ($author || $role) : ?>
<cite class="testimonial-author">
<span class="author-name"><?php echo esc_html($author); ?></span>
<?php if ($role) : ?>
<span class="author-role"><?php echo esc_html($role); ?></span>
<?php endif; ?>
</cite>
<?php endif; ?>
</div>
blocks/testimonial/index.js:
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl, TextControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
registerBlockType('my-theme/testimonial', {
edit: ({ attributes, setAttributes }) => {
const { content, author, role, rating } = attributes;
const blockProps = useBlockProps();
return (
<>
<InspectorControls>
<PanelBody title={__('Settings', 'my-theme')}>
<TextControl
label={__('Author', 'my-theme')}
value={author}
onChange={(v) => setAttributes({ author: v })}
/>
<TextControl
label={__('Role', 'my-theme')}
value={role}
onChange={(v) => setAttributes({ role: v })}
/>
<RangeControl
label={__('Rating', 'my-theme')}
value={rating}
onChange={(v) => setAttributes({ rating: v })}
min={1}
max={5}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<RichText
tagName="blockquote"
value={content}
onChange={(v) => setAttributes({ content: v })}
placeholder={__('Testimonial text...', 'my-theme')}
/>
<div className="testimonial-rating">
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
onClick={() => setAttributes({ rating: star })}
>
{star <= rating ? '★' : '☆'}
</span>
))}
</div>
<cite>
<RichText
tagName="span"
value={author}
onChange={(v) => setAttributes({ author: v })}
placeholder={__('Author', 'my-theme')}
/>
</cite>
</div>
</>
);
},
save: () => null, // Server-side rendering
});
functions.php:
add_action('init', 'register_custom_blocks');
function register_custom_blocks() {
register_block_type(__DIR__ . '/blocks/testimonial');
}
Common controls for block settings:
import {
InspectorControls,
PanelColorSettings,
MediaUpload
} from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
ToggleControl,
RangeControl,
Button
} from '@wordpress/components';
<InspectorControls>
<PanelBody title="Layout">
<SelectControl
label="Columns"
value={columns}
options={[
{ label: '2', value: 2 },
{ label: '3', value: 3 },
{ label: '4', value: 4 }
]}
onChange={(v) => setAttributes({ columns: parseInt(v) })}
/>
<ToggleControl
label="Enable Shadow"
checked={enableShadow}
onChange={(v) => setAttributes({ enableShadow: v })}
/>
<RangeControl
label="Border Radius"
value={borderRadius}
onChange={(v) => setAttributes({ borderRadius: v })}
min={0}
max={50}
/>
</PanelBody>
<PanelBody title="Media">
<MediaUpload
onSelect={(media) => setAttributes({ imageUrl: media.url })}
allowedTypes={['image']}
render={({ open }) => (
<Button onClick={open} variant="secondary">
{imageUrl ? 'Change Image' : 'Select Image'}
</Button>
)}
/>
</PanelBody>
<PanelColorSettings
title="Colors"
colorSettings={[
{
value: bgColor,
onChange: (v) => setAttributes({ bgColor: v }),
label: 'Background'
}
]}
/>
</InspectorControls>
Enable WordPress features:
"supports": {
"html": false,
"anchor": true,
"align": ["wide", "full"],
"color": {
"background": true,
"text": true,
"gradients": true
},
"spacing": {
"padding": true,
"margin": true,
"blockGap": true
},
"typography": {
"fontSize": true,
"lineHeight": true,
"fontWeight": true
}
}
add_action('init', 'register_book_cpt');
function register_book_cpt() {
register_post_type('book', [
'labels' => [
'name' => __('Books', 'my-theme'),
'singular_name' => __('Book', 'my-theme'),
],
'public' => true,
'has_archive' => true,
'supports' => ['title', 'editor', 'thumbnail'],
'show_in_rest' => true, // REQUIRED for block editor
'menu_icon' => 'dashicons-book',
'template' => [ // Default blocks
['core/paragraph', ['placeholder' => 'Book description...']],
['core/image'],
['my-theme/book-details'],
],
'template_lock' => 'insert', // Can't add/remove blocks
]);
// Register taxonomy
register_taxonomy('genre', 'book', [
'labels' => ['name' => __('Genres', 'my-theme')],
'hierarchical' => true,
'show_in_rest' => true, // REQUIRED
]);
}
false: No restrictions'all': Cannot modify structure'insert': Cannot add/remove, can reorder'contentOnly': Content edits only"customTemplates": [
{
"name": "single-book",
"title": "Book Template",
"postTypes": ["book"]
}
]
package.json:
{
"scripts": {
"start": "wp-scripts start",
"build": "wp-scripts build"
},
"devDependencies": {
"@wordpress/scripts": "^27.0.0"
}
}
Commands:
npm install
npm run start # Development with hot reload
npm run build # Production build (minified)
.wp-env.json:
{
"core": "WordPress/WordPress#6.7",
"phpVersion": "8.3",
"themes": ["./my-block-theme"],
"config": {
"WP_DEBUG": true,
"SCRIPT_DEBUG": true
}
}
Usage:
npx @wordpress/env start
# Access: http://localhost:8888
# Admin: admin / password
npx @wordpress/env stop
npx @wordpress/env clean # Reset database
| Classic | Block Equivalent |
|---------|------------------|
| the_title() | <!-- wp:post-title /--> |
| the_content() | <!-- wp:post-content /--> |
| the_post_thumbnail() | <!-- wp:post-featured-image /--> |
| the_date() | <!-- wp:post-date /--> |
| wp_nav_menu() | <!-- wp:navigation /--> |
| get_header() | <!-- wp:template-part {"slug":"header"} /--> |
| get_footer() | <!-- wp:template-part {"slug":"footer"} /--> |
| get_sidebar() | <!-- wp:template-part {"slug":"sidebar"} /--> |
add_theme_support('wp-block-styles');
add_theme_support('align-wide');
add_theme_support('responsive-embeds');
WordPress validates block markup against registered block definitions. Invalid blocks show errors in the editor:
Common validation errors:
Fix validation errors:
// Add deprecated versions for backward compatibility
const deprecated = [
{
attributes: {
oldName: { type: 'string' }
},
migrate: (attributes) => ({
newName: attributes.oldName
}),
save: (props) => {
// Old save function
}
}
];
✅ Use server-side rendering (render.php) when possible
✅ Leverage block supports (reduces custom CSS)
✅ Disable unused features: "defaultPalette": false
✅ Use CSS custom properties for consistency
❌ Avoid client-side rendering for static content
❌ Don't override core blocks with !important
✅ Semantic HTML (<header>, <main>, <footer>)
✅ Keyboard navigation for custom blocks
✅ WCAG AA color contrast (4.5:1 minimum)
✅ Alt text for all images
❌ Don't assume FSE = accessible (test required)
❌ Mixing classic and block approaches
❌ Hardcoding colors (use CSS variables)
❌ Reinventing block supports
❌ Skipping accessibility testing
❌ Using get_header() in HTML templates
WordPress: 6.7+ | PHP: 8.1+ | Tools: @wordpress/scripts, wp-env
development
Optimize web performance using Core Web Vitals, modern patterns (View Transitions, Speculation Rules), and framework-specific techniques
development
Best practices for documenting APIs and code interfaces, eliminating redundant documentation guidance per agent.
development
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices
development
Visual verification workflow for UI changes to accelerate code review and catch ...