---
name: wp-block-development
description: WordPress block editor code review and Gutenberg block development patterns for WordPress 6.x+. Use when reviewing block code, auditing block.json schema, checking editor components, validating render callbacks, analyzing block attributes, verifying InnerBlocks usage, detecting block validation errors, reviewing Interactivity API directives, or when user mentions "block review", "Gutenberg", "block development", "block editor", "block.json", "useBlockProps", "InnerBlocks", "Interactivity API", "data-wp-bind", "render_callback", "dynamic block", "static block", "block deprecation", "block attributes", "block supports", "@wordpress/scripts", "wp-scripts", "block validation error", "save function", "RichText", "InspectorControls", "BlockControls". Detects issues in block.json schema, React/JSX editor patterns, server-side rendering, attribute handling, and frontend interactions.
---
# WordPress Block Development Review Skill
## Overview
Systematic block development review for WordPress 6.x+ block editor (Gutenberg). **Core principle:** WordPress blocks follow a dual-architecture pattern—React components for the editor (edit function) and either static HTML (save function) or server-side PHP (render_callback/render file) for the frontend. block.json is the single source of truth. Review validates block.json schema, editor patterns (React/JSX), server-side rendering (PHP), attribute handling, deprecation management, and Interactivity API usage. Report findings grouped by file (PHP and JS/JSX files intermixed by actual path) with line numbers, severity labels (CRITICAL/WARNING/INFO), and BAD/GOOD code pairs.
**Note:** This skill reviews BOTH PHP and JavaScript/React code. PHP follows WordPress PHP Coding Standards (spaces in parentheses, `array()` not `[]`, Yoda conditions). JavaScript/JSX follows WordPress JS coding standards (tab indentation, JSDoc comments, camelCase for variables/functions, PascalCase for components).
## When to Use
**Use when:**
- Block plugin code review (single block, multi-block, or block library)
- block.json schema validation and field verification
- Editor component review (edit/save functions, React/JSX patterns)
- Render callback or render file audit (server-side PHP)
- InnerBlocks pattern review (nested blocks, template, templateLock)
- Block deprecation check (save function migrations)
- Interactivity API directive review (WP 6.5+ frontend interactions)
- Block attribute schema validation (type, source, selector, default)
- @wordpress/scripts build configuration check
- Block validation error investigation
- useBlockProps, RichText, InspectorControls, BlockControls usage
**Don't use for:**
- theme.json configuration (use wp-theme-development when available)
- General React application review (this is block-editor-specific)
- WooCommerce block extensions (use wp-woocommerce-dev when available)
- Security-only audits (use wp-security-review for comprehensive security analysis)
- Plugin architecture audits (use wp-plugin-development for plugin structure)
- Performance-only audits (use wp-performance-review)
## Code Review Workflow
Follow this seven-step workflow for systematic block reviews:
1. **Identify block type and context**
- Single block plugin → One block.json, simple structure
- Multi-block plugin → Multiple blocks in src/block-one/, src/block-two/
- Block library/collection → Published as package, namespacing critical
- Theme blocks → Registered in functions.php, different loading context
2. **Validate block.json schema (BLK-02, BLK-06, BLK-08, BLK-10)**
- apiVersion must be 3 (WP 6.3+). Flag 1 or 2 as WARNING with upgrade guidance.
- name must be "namespace/block-name" format (lowercase, letters, numbers, dashes)
- title, category required
- attributes: validate type, source, selector, default values. Flag type mismatches.
- supports: color, spacing, typography, align, anchor, html
- editorScript, script, viewScript, viewScriptModule: validate "file:./path" format
- style, editorStyle: validate paths
- render: "file:./render.php" for dynamic blocks
- $schema field recommended for validation
3. **Check edit function (BLK-05, BLK-20)**
- useBlockProps() MUST be called and spread on wrapper element
- Import pattern: @wordpress/* packages (GOOD) vs window.wp.* (BAD - legacy)
- InspectorControls for sidebar settings, BlockControls for toolbar
- RichText proper usage (tagName, value, onChange, allowedFormats)
- InnerBlocks with allowedBlocks and template props
- useSelect/useDispatch from @wordpress/data (combine selectors in single useSelect call)
- i18n: all user-facing strings wrapped in __() from @wordpress/i18n with text domain
4. **Check save function or render callback**
- **Static blocks:** useBlockProps.save() MUST be called, RichText.Content for rich text, InnerBlocks.Content for nested blocks
- **Dynamic blocks:** save returns null (or InnerBlocks.Content if nested blocks used - BLK-14 CRITICAL)
- Save function must be DETERMINISTIC - no random values, no Date.now(), no side effects
- **Render callback/render.php:** get_block_wrapper_attributes() for wrapper, escape all output (esc_html, esc_attr, esc_url, wp_kses_post but NOT on $content from InnerBlocks - BLK-21), defined( 'ABSPATH' ) || exit; at top
5. **Scan for CRITICAL patterns**
- Missing apiVersion or using 1/2 instead of 3
- window.wp.* instead of @wordpress/* imports in JS/JSX
- save function without useBlockProps.save() (apiVersion 3)
- render callback using wp_kses_post() on $content parameter (breaks embeds)
- Dynamic block with InnerBlocks but save returns null (no InnerBlocks.Content)
- Attribute with source: 'meta' (deprecated - use useEntityProp)
- Invalid attribute type/source combination
- Missing register_block_type() call in PHP
6. **Check WARNING patterns**
- block.json missing supports field
- edit function without useBlockProps()
- Hardcoded strings not wrapped in __() in JS/JSX or PHP
- render.php without get_block_wrapper_attributes()
- PHP render files without defined( 'ABSPATH' ) check
- save function with side effects (Math.random, Date.now)
- Missing block.json $schema field
- useSelect with multiple separate calls (performance anti-pattern)
7. **Note INFO improvements**
- Using render_callback in PHP instead of render file in block.json
- Missing viewScript/viewScriptModule in block.json (no frontend JS)
- apiVersion 2 (suggest upgrade to 3)
- Missing keywords in block.json
- Not using block hooks for auto-insertion opportunities
Report using output format below. If security concerns found (unescaped render output, user input in Interactivity API state), add note: "Security issues detected. Run `/wp-sec-review` for comprehensive security analysis." If plugin architecture issues found (init hook registration, ABSPATH check missing), add note: "Plugin architecture issues detected. Run `/wp-plugin-review` for comprehensive plugin review."
**Source vs Build Review:** Review src/ files for code patterns (developer intent). Flag if build/ directory is missing or stale (check index.asset.php timestamp vs src/ modification times). Do NOT review build/ files for code quality - they are compiled output.
## File-Type Specific Checks
### block.json (BLK-02, BLK-06, BLK-08, BLK-10)
**apiVersion field:**
- CRITICAL: Missing apiVersion → Block won't register correctly
- WARNING: apiVersion 1 or 2 → Upgrade to 3 for iframe isolation (WP 6.3+)
- Pattern: `"apiVersion": 3`
**name field:**
- CRITICAL: Missing name → Block won't register
- CRITICAL: Invalid format (not "namespace/block-name") → Registration fails
- Pattern: `"name": "my-plugin/my-block"` (lowercase, dashes, letters, numbers)
**attributes field:**
- CRITICAL: Invalid type (not string/number/boolean/object/array/integer/null) → Validation fails
- CRITICAL: source:'meta' → Deprecated, use useEntityProp hook instead
- WARNING: type doesn't match source data type → Attribute won't populate correctly
- Pattern: Validate type, source, selector, default combinations
- Sources: attribute, text, html, query (meta deprecated)
**supports field:**
- WARNING: Missing supports → Users can't customize color/spacing/typography
- INFO: Could add common supports (color, spacing, typography, align, anchor)
- Pattern: Object with nested configuration for each support type
**editorScript/script/viewScript/viewScriptModule fields:**
- WARNING: Invalid path format → Assets won't load
- Pattern: `"file:./index.js"` relative to block.json location
- Note: viewScriptModule for Interactivity API (WP 6.5+)
**render field:**
- INFO: Dynamic blocks can use render file instead of render_callback
- Pattern: `"file:./render.php"` relative to block.json location
**$schema field:**
- INFO: Missing $schema → Can't validate schema in IDE
- Pattern: `"$schema": "https://schemas.wp.org/trunk/block.json"`
### Edit function / edit.js (BLK-05, BLK-20)
**useBlockProps usage:**
- CRITICAL: useBlockProps() not called → Block won't render properly in editor
- CRITICAL: useBlockProps result not spread on wrapper → Missing block classes/attributes
- Pattern: `const blockProps = useBlockProps();` then `
`
**Import patterns:**
- WARNING: window.wp.* global access → Legacy pattern, breaks modern builds
- Pattern: GOOD: `import { useBlockProps } from '@wordpress/block-editor';` BAD: `const { useBlockProps } = window.wp.blockEditor;`
**InspectorControls and BlockControls:**
- INFO: Could add settings sidebar with InspectorControls
- INFO: Could add toolbar controls with BlockControls
- Pattern: InspectorControls for PanelBody/ToggleControl/SelectControl, BlockControls for AlignmentToolbar/ToolbarGroup
**RichText usage:**
- WARNING: RichText without tagName → May render incorrectly
- WARNING: RichText without value/onChange → Not controlled component
- Pattern: ` setAttributes( { content } ) } />`
**InnerBlocks usage:**
- INFO: Consider allowedBlocks to restrict nesting
- INFO: Consider template for default block structure
- Pattern: ``
**useSelect/useDispatch performance:**
- WARNING: Multiple separate useSelect calls → Performance degradation with many blocks
- Pattern: Combine selectors in single useSelect when reading multiple values
**Internationalization:**
- WARNING: Hardcoded strings without __() → Not translatable
- Pattern: `__( 'Text', 'text-domain' )` for all user-facing strings
### Save function / save.js (BLK-05, BLK-07)
**Static blocks (save returns JSX):**
- CRITICAL: Missing useBlockProps.save() → Block validation error (apiVersion 3)
- WARNING: RichText without RichText.Content → Won't save rich text correctly
- WARNING: InnerBlocks without InnerBlocks.Content → Nested blocks won't save
- Pattern: `const blockProps = useBlockProps.save();` then `
`
**Dynamic blocks (save returns null or InnerBlocks.Content):**
- CRITICAL: Dynamic block with InnerBlocks but save returns null → Nested blocks lost (BLK-14)
- Pattern: If block uses InnerBlocks, save MUST return `` even for dynamic blocks
**Deterministic save:**
- WARNING: Math.random() or Date.now() in save → Block validation errors on re-save
- WARNING: Side effects in save → Unpredictable behavior
- Pattern: Save must return identical markup for identical attributes
### Render callback / render.php (BLK-11)
**ABSPATH check:**
- WARNING: Missing defined( 'ABSPATH' ) || exit; → Direct file access possible
- Pattern: First line after >`
**Output escaping:**
- CRITICAL: Unescaped output → XSS vulnerability (CWE-79)
- CRITICAL: wp_kses_post() on $content from InnerBlocks → Breaks embeds (BLK-21)
- Pattern: esc_html() for text, esc_attr() for attributes, esc_url() for URLs, wp_kses_post() for trusted HTML (but NOT InnerBlocks $content)
- Cross-reference: See wp-security-review for comprehensive escaping patterns
**Function parameters:**
- $attributes: Array of block attributes
- $content: InnerBlocks HTML (if InnerBlocks used)
- $block: WP_Block instance with context and other metadata
- Pattern: Use all three parameters for full dynamic block functionality
### Plugin main file / plugin.php
**register_block_type():**
- CRITICAL: Missing register_block_type() → Block won't register
- WARNING: Not hooked to 'init' → May register too early
- Pattern: `add_action( 'init', 'callback' );` then `register_block_type( __DIR__ . '/build/block-name' );`
**Multi-block registration:**
- WARNING: Single register_block_type() for multi-block plugin → Other blocks won't register
- Pattern: Call register_block_type() for each block's build directory
**Plugin header and ABSPATH:**
- Cross-reference: See wp-plugin-development for plugin header requirements and ABSPATH check
### Block deprecation (BLK-09)
**deprecated array:**
- CRITICAL: Save function changed without deprecation → Block validation errors for existing content
- WARNING: Missing apiVersion in deprecation entry → Deprecation may not match
- Pattern: deprecated array with save + attributes for each version
**migrate function:**
- INFO: Could add migrate function for attribute schema changes
- Pattern: `migrate( attributes ) { return { ...attributes, newField: 'default' }; }`
**Ordering:**
- WARNING: Deprecations ordered oldest first → WordPress checks newest first, performance issue
- Pattern: Newest deprecation listed FIRST in array
### Interactivity API files (BLK-12)
**WP 6.5+ version marker:**
- INFO: Interactivity API requires WordPress 6.5+ → Add version check or plugin requirement
- Pattern: Check `Requires at least: 6.5` in plugin header
**render.php directives:**
- data-wp-interactive="namespace" → REQUIRED for Interactivity API scope
- wp_interactivity_state() → Server-side state initialization
- wp_interactivity_config() → Server-side configuration
- data-wp-context → Context provider for nested elements
- Directives: data-wp-bind, data-wp-on, data-wp-class, data-wp-style, data-wp-text, data-wp-watch, data-wp-init, data-wp-each
**view.js (frontend store):**
- WARNING: Import from window.wp.interactivity → Legacy pattern, use @wordpress/interactivity
- Pattern: `import { store } from '@wordpress/interactivity';` then `store( 'namespace', { actions, state } );`
**Key distinction:**
- Interactivity API = FRONTEND ONLY (view.js, render.php directives)
- Editor still uses React (edit.js)
- Don't confuse frontend state management with editor component state
**Security crossover:**
- WARNING: User input in wp_interactivity_state() without sanitization → XSS risk
- Cross-reference: See wp-security-review for input sanitization patterns
### Block context (BLK-16)
**providesContext (parent block):**
- Pattern: In parent block.json: `"providesContext": { "myPlugin/keyName": "attributeName" }`
- Namespace context keys: Use "myPlugin/keyName" format
**usesContext (child block):**
- Pattern: In child block.json: `"usesContext": [ "myPlugin/keyName" ]`
- Access in edit: `function Edit( { context } ) { const value = context['myPlugin/keyName']; }`
### Surface-level checks
**Block transforms (BLK-13 partial):**
- INFO: Could add transforms for conversion from/to other blocks
- Pattern: from/to array in block registration
**Block variations (BLK-13):**
- INFO: Could add variations for preset configurations
- Pattern: variation picker, isDefault, scope properties
**Block styles:**
- INFO: Could register style variations in block.json or PHP
- Pattern: styles array in block.json or register_block_style() in PHP
**Block patterns (BLK-18):**
- Pattern: register_block_pattern() structure, categories, keywords
- Note: Patterns are multi-block layouts, different from variations (single block presets)
**Block hooks (BLK-19):**
- INFO: Could use blockHooks for auto-insertion (WP 6.4+)
- CRITICAL: Block hooks only work with dynamic blocks (save returns null)
- Pattern: blockHooks property in block.json
**Block Bindings API (BLK-17):**
- INFO: Could use bindings for dynamic attribute connections (WP 6.5+)
- Pattern: Surface-level detection only
**@wordpress/scripts build config:**
- WARNING: Missing @wordpress/scripts → No build toolchain
- WARNING: package.json missing build/start scripts → Can't compile blocks
- Pattern: Check package.json for "scripts": { "build": "wp-scripts build", "start": "wp-scripts start" }
**Template lock:**
- INFO: Could use templateLock on InnerBlocks for fixed layouts
- Pattern: `` prevents block addition/removal/reordering
## Search Patterns for Quick Detection (BLK-22)
Use these `rg` commands for quick block scanning. Organized by severity. Cover BOTH PHP and JS/JSX files.
### CRITICAL Patterns
```bash
# block.json without apiVersion 3
rg -l --iglob 'block.json' . | xargs -I{} sh -c "rg -q '\"apiVersion\"\\s*:\\s*3' '{}' || echo '{}'"
# JS/JSX files with window.wp.* instead of @wordpress/* imports
rg -n "window\.wp\." . -g '*.{js,jsx}'
# save function without useBlockProps.save() in JSX files (manual candidate list)
rg -n "function save|const save\s*=|save:\s*\(" . -g '*.{js,jsx}'
# render callback using wp_kses_post on $content parameter (breaks embeds)
rg -n "wp_kses_post\s*\(\s*\$content\s*\)" . -g '*.php'
# Dynamic block with InnerBlocks but save returns null
# Manual check: if edit.js uses InnerBlocks, save.js should include InnerBlocks.Content.
rg -n "InnerBlocks" . -g '*.{js,jsx}'
rg -n "return\s+null" . -g '*.{js,jsx}'
# Attribute with source: 'meta' (deprecated)
rg -n "\"source\"\s*:\s*\"meta\"" . -g 'block.json'
# Missing register_block_type() call in plugin bootstrap files
rg -n "register_block_type\s*\(" . -g '*.php'
```
### WARNING Patterns
```bash
# block.json missing supports field
rg -l --iglob 'block.json' . | xargs -I{} sh -c "rg -q '\"supports\"' '{}' || echo '{}'"
# edit function without useBlockProps() in JS/JSX files (manual candidate list)
rg -n "function Edit|export default function Edit|const Edit\s*=" . -g '*.{js,jsx}'
# Hardcoded strings not wrapped in __() in JS/JSX files
rg -n "title\s*:\s*['\"][A-Z]" . -g '*.{js,jsx}'
# render.php without get_block_wrapper_attributes()
rg -l --iglob 'render.php' . | xargs -I{} sh -c "rg -q 'get_block_wrapper_attributes' '{}' || echo '{}'"
# PHP render files without defined( 'ABSPATH' ) check
rg -l --iglob 'render.php' . | xargs -I{} sh -c "rg -q 'defined.*ABSPATH' '{}' || echo '{}'"
# save function with side effects (Math.random, Date.now)
rg -n "Math\.random|Date\.now|new Date\(" . -g 'save.js'
# Missing block.json $schema field
rg -l --iglob 'block.json' . | xargs -I{} sh -c "rg -q '\"\\$schema\"' '{}' || echo '{}'"
# useSelect with multiple separate calls (performance anti-pattern)
# Manual check: inspect files with repeated useSelect() calls.
rg -n "useSelect\s*\(" . -g '*.{js,jsx}'
```
### INFO Patterns
```bash
# Using render_callback in PHP instead of render file in block.json
grep -rn "render_callback" --include="*.php" .
# Missing viewScript/viewScriptModule in block.json (no frontend JS)
grep -L "viewScript\|viewScriptModule" --include="block.json" .
# apiVersion 2 (suggest upgrade to 3)
grep -rn "\"apiVersion\": 2" --include="block.json" .
# Missing keywords in block.json
grep -L "\"keywords\"" --include="block.json" .
# Not using block hooks for auto-insertion opportunities
# (Manual check: Review block purpose for auto-insertion potential)
```
**Note:** Grep patterns for JavaScript require different regex than PHP patterns. Use `--include="*.js" --include="*.jsx"` for JS files, `--include="*.php"` for PHP files.
## Block Context Detection
Context-aware review notes based on block distribution and loading context:
### Single block plugin
**Structure:** One block.json at root or src/, single build output
**Review adjustments:** Standard review, no special considerations
**Pattern:** Plugin folder contains single src/ directory with index.js, edit.js, save.js
### Multi-block plugin
**Structure:** Multiple blocks in src/block-one/, src/block-two/, each with block.json
**Review adjustments:**
- Check shared components in src/shared/ or src/components/
- Verify consistent naming conventions across all blocks
- Ensure build config handles all blocks (multiple entry points or wildcard)
- Verify register_block_type() called for each block's build directory
**Pattern:** Plugin folder contains multiple src/*/block.json files
### Block library/collection
**Structure:** Published as npm package or WordPress.org plugin with multiple blocks
**Review adjustments:**
- Namespacing CRITICAL (all blocks must use consistent namespace)
- Check for consistent naming: "namespace/block-one", "namespace/block-two"
- Verify all blocks in same namespace
- Package.json should have "main" field pointing to build entry
**Pattern:** package.json has "name" field, published to npm or WordPress.org
### Theme blocks
**Structure:** Registered in functions.php, block files in /blocks/ or /inc/blocks/
**Review adjustments:**
- Different loading context (theme activation vs plugin activation)
- Block may depend on theme.json settings (colors, spacing, typography)
- Brief note on theme.json integration, defer depth to Phase 4 (Theme Development)
- Verify blocks registered on 'init' or 'after_setup_theme' hook
**Pattern:** register_block_type() in functions.php, blocks in theme subdirectory
## Quick Reference: Block Development Patterns (BLK-23)
Common block patterns organized by concern. All examples use WordPress coding standards (PHP: spaces in parentheses, `array()` not `[]`, Yoda conditions; JS: tab indentation, camelCase, PascalCase for components).
### block.json Schema
Complete schema with all fields annotated (apiVersion 3):
```json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "my-plugin/my-block",
"title": "My Block",
"category": "widgets",
"icon": "smiley",
"description": "A custom block example",
"keywords": [ "custom", "example" ],
"version": "1.0.0",
"textdomain": "my-plugin",
"attributes": {
"content": {
"type": "string",
"source": "html",
"selector": "p",
"default": ""
},
"showImage": {
"type": "boolean",
"default": false
}
},
"supports": {
"html": false,
"color": {
"background": true,
"text": true
},
"spacing": {
"margin": true,
"padding": true
},
"typography": {
"fontSize": true,
"lineHeight": true
},
"align": [ "wide", "full" ],
"anchor": true
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css",
"viewScriptModule": "file:./view.js"
}
```
### Block Registration
PHP + JS registration pattern:
**❌ BAD: Missing ABSPATH check, not hooked to init**
```php
setAttributes( { content: e.target.value } ) }
/>
;
}
```
## FILE: src/save.js
### Line 5: CRITICAL - Missing useBlockProps.save()
apiVersion 3 blocks require useBlockProps.save(). Block validation errors will occur.
❌ **BAD:**
```javascript
function save() {
return
Content
;
}
```
✅ **GOOD:**
```javascript
import { useBlockProps } from '@wordpress/block-editor';
function save() {
const blockProps = useBlockProps.save();
return
Content
;
}
```
## FILE: render.php
### Line 15: CRITICAL - wp_kses_post on InnerBlocks content
Using wp_kses_post() on $content breaks embed blocks and oEmbed processing. InnerBlocks content is already sanitized.
❌ **BAD:**
```php
```
✅ **GOOD:**
```php
```
### Line 1: WARNING - Missing ABSPATH check
Add ABSPATH check at top of file to prevent direct access.
❌ **BAD:**
```php