Agent Skills by ALSEL
汎用ソフトウェア開発⭐ リポ 3品質スコア 76/100

wp-block-development

WordPress ブロックエディタのコードレビューおよび WordPress 6.x 以降の Gutenberg ブロック開発パターンに対応します。ブロックコードのレビュー、block.json スキーマの監査、エディタコンポーネントの確認、レンダーコールバックの検証、ブロック属性の分析、InnerBlocks の使用確認、ブロック検証エラーの検出、Interactivity API ディレクティブのレビュー時に使用できます。また、「ブロックレビュー」「Gutenberg」「ブロック開発」「ブロックエディタ」「block.json」「useBlockProps」「InnerBlocks」「Interactivity API」「data-wp-bind」「render_callback」「動的ブロック」「静的ブロック」「ブロック廃止予定」「ブロック属性」「ブロックサポート」「@wordpress/scripts」「wp-scripts」「ブロック検証エラー」「save関数」「RichText」「InspectorControls」「BlockControls」などのキーワードが含まれる場合も対応します。block.json スキーマの問題、React/JSX エディタパターン、サーバー側レンダリング、属性処理、フロントエンドインタラクションの問題を検出できます。

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.

SKILL.md 本文

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 <div { ...blockProps }>

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: <RichText tagName="p" value={ attributes.content } onChange={ ( content ) => setAttributes( { content } ) } />

InnerBlocks usage:

  • INFO: Consider allowedBlocks to restrict nesting
  • INFO: Consider template for default block structure
  • Pattern: <InnerBlocks allowedBlocks={ [ 'core/paragraph' ] } template={ [ [ 'core/heading' ] ] } />

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 <div { ...blockProps }>

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 <InnerBlocks.Content /> 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 <?php should be ABSPATH check
  • Cross-reference: See wp-security-review for security depth

get_block_wrapper_attributes():

  • WARNING: Manual class concatenation instead of get_block_wrapper_attributes() → Missing block supports
  • Pattern: $wrapper_attributes = get_block_wrapper_attributes(); then <div <?php echo $wrapper_attributes; ?>>

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: <InnerBlocks templateLock="all" /> 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

# 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

# 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

# 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):

{
	"$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
function my_plugin_register_block() {
	register_block_type( __DIR__ . '/build/my-block' );
}
my_plugin_register_block(); // Runs immediately, not on init

✅ GOOD: Complete registration with ABSPATH and init hook

<?php
defined( 'ABSPATH' ) || exit;

function my_plugin_register_blocks() {
	register_block_type( __DIR__ . '/build/my-block' );
}
add_action( 'init', 'my_plugin_register_blocks' );

✅ GOOD: Multi-block registration

<?php
defined( 'ABSPATH' ) || exit;

function my_plugin_register_blocks() {
	register_block_type( __DIR__ . '/build/block-one' );
	register_block_type( __DIR__ . '/build/block-two' );
	register_block_type( __DIR__ . '/build/block-three' );
}
add_action( 'init', 'my_plugin_register_blocks' );

Static Block Pattern

Edit with useBlockProps, save with useBlockProps.save, RichText + RichText.Content:

❌ BAD: Missing useBlockProps in edit and save

// edit.js
export default function Edit( { attributes, setAttributes } ) {
	return (
		<div className="my-block">
			<input
				value={ attributes.content }
				onChange={ ( e ) => setAttributes( { content: e.target.value } ) }
			/>
		</div>
	);
}

// save.js
export default function save( { attributes } ) {
	return (
		<div className="my-block">
			<p>{ attributes.content }</p>
		</div>
	);
}

✅ GOOD: Complete static block with useBlockProps and RichText

// edit.js
import { useBlockProps, RichText } from '@wordpress/block-editor';

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..."
			/>
		</div>
	);
}

// save.js
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>
	);
}

Dynamic Block Pattern

Edit with useBlockProps, save returns null (or InnerBlocks.Content), render.php with get_block_wrapper_attributes and escaping:

❌ BAD: render callback without wrapper attributes, unescaped output

<?php
function my_plugin_render_callback( $attributes ) {
	return '<div><p>' . $attributes['content'] . '</p></div>';
}

✅ GOOD: Complete dynamic block with proper escaping

<?php
// render.php
defined( 'ABSPATH' ) || exit;

$wrapper_attributes = get_block_wrapper_attributes();
?>
<div <?php echo $wrapper_attributes; ?>>
	<p><?php echo esc_html( $attributes['content'] ); ?></p>
</div>
// save.js - Dynamic blocks return null
export default function save() {
	return null;
}

✅ GOOD: Dynamic block registered with render file

<?php
// plugin.php
defined( 'ABSPATH' ) || exit;

function my_plugin_register_blocks() {
	register_block_type( __DIR__ . '/build/my-block' );
	// render file specified in block.json "render": "file:./render.php"
}
add_action( 'init', 'my_plugin_register_blocks' );

InnerBlocks Pattern

allowedBlocks, template, templateLock, InnerBlocks.Content in save (even for dynamic blocks):

❌ BAD: Dynamic block with InnerBlocks but save returns null

// edit.js
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';

export default function Edit() {
	const blockProps = useBlockProps();
	return (
		<div { ...blockProps }>
			<InnerBlocks />
		</div>
	);
}

// save.js - CRITICAL: Nested blocks will be lost!
export default function save() {
	return null;
}

✅ GOOD: Dynamic block with InnerBlocks saves nested content

// edit.js
import { useBlockProps, InnerBlocks } from '@wordpress/block-editor';

export default function Edit() {
	const blockProps = useBlockProps();
	const ALLOWED_BLOCKS = [ 'core/paragraph', 'core/image', 'core/heading' ];
	const TEMPLATE = [
		[ 'core/heading', { placeholder: 'Card Title' } ],
		[ 'core/paragraph', { placeholder: 'Card content...' } ],
	];

	return (
		<div { ...blockProps }>
			<InnerBlocks
				allowedBlocks={ ALLOWED_BLOCKS }
				template={ TEMPLATE }
				templateLock={ false }
			/>
		</div>
	);
}

// save.js - MUST save InnerBlocks.Content even for dynamic blocks
import { InnerBlocks } from '@wordpress/block-editor';

export default function save() {
	return <InnerBlocks.Content />;
}

✅ GOOD: render.php uses $content parameter for InnerBlocks HTML

<?php
// render.php
defined( 'ABSPATH' ) || exit;

$wrapper_attributes = get_block_wrapper_attributes();
?>
<div <?php echo $wrapper_attributes; ?>>
	<h3><?php echo esc_html( $attributes['title'] ); ?></h3>
	<?php echo $content; // InnerBlocks HTML - DO NOT use wp_kses_post() ?>
</div>

useBlockProps

edit (useBlockProps()) and save (useBlockProps.save()), merging custom props:

❌ BAD: Missing useBlockProps

export default function Edit() {
	return <div className="my-block">Content</div>;
}

function save() {
	return <div className="my-block">Content</div>;
}

✅ GOOD: useBlockProps with custom props merged

import { useBlockProps } from '@wordpress/block-editor';

export default function Edit( { attributes } ) {
	const blockProps = useBlockProps( {
		className: 'my-custom-class',
		style: { backgroundColor: attributes.bgColor },
	} );

	return <div { ...blockProps }>Content</div>;
}

function save( { attributes } ) {
	const blockProps = useBlockProps.save( {
		className: 'my-custom-class',
		style: { backgroundColor: attributes.bgColor },
	} );

	return <div { ...blockProps }>Content</div>;
}

Attributes

type/source/selector/default patterns, attribute type matching:

❌ BAD: Type mismatch, no default, deprecated source

{
	"attributes": {
		"count": {
			"type": "number",
			"source": "text",
			"selector": ".count"
		},
		"metaValue": {
			"type": "string",
			"source": "meta",
			"meta": "my_meta_key"
		}
	}
}

✅ GOOD: Correct type/source matching, defaults, modern patterns

{
	"attributes": {
		"content": {
			"type": "string",
			"source": "html",
			"selector": "p",
			"default": ""
		},
		"count": {
			"type": "number",
			"source": "attribute",
			"selector": ".count",
			"attribute": "data-count",
			"default": 0
		},
		"url": {
			"type": "string",
			"source": "attribute",
			"selector": "a",
			"attribute": "href",
			"default": ""
		},
		"items": {
			"type": "array",
			"source": "query",
			"selector": "li",
			"query": {
				"text": {
					"type": "string",
					"source": "text"
				}
			},
			"default": []
		}
	}
}

✅ GOOD: Meta attribute using useEntityProp (not deprecated source)

import { useEntityProp } from '@wordpress/core-data';

export default function Edit() {
	const [ meta, setMeta ] = useEntityProp( 'postType', 'post', 'meta' );
	const value = meta.my_meta_key;

	const updateMeta = ( newValue ) => {
		setMeta( { ...meta, my_meta_key: newValue } );
	};

	// Use value and updateMeta in component
}

Block Controls

InspectorControls for sidebar, BlockControls for toolbar, PanelBody, ToggleControl, SelectControl:

❌ BAD: Settings hardcoded in block markup, no sidebar controls

export default function Edit( { attributes, setAttributes } ) {
	return (
		<div>
			<label>
				Show Image:
				<input
					type="checkbox"
					checked={ attributes.showImage }
					onChange={ ( e ) => setAttributes( { showImage: e.target.checked } ) }
				/>
			</label>
		</div>
	);
}

✅ GOOD: InspectorControls for sidebar, BlockControls for toolbar

import { useBlockProps, InspectorControls, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
import { PanelBody, ToggleControl, SelectControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

export default function Edit( { attributes, setAttributes } ) {
	const blockProps = useBlockProps();

	return (
		<>
			<BlockControls>
				<AlignmentToolbar
					value={ attributes.align }
					onChange={ ( align ) => setAttributes( { align } ) }
				/>
			</BlockControls>

			<InspectorControls>
				<PanelBody title={ __( 'Settings', 'my-plugin' ) }>
					<ToggleControl
						label={ __( 'Show featured image', 'my-plugin' ) }
						checked={ attributes.showImage }
						onChange={ ( showImage ) => setAttributes( { showImage } ) }
					/>
					<SelectControl
						label={ __( 'Display style', 'my-plugin' ) }
						value={ attributes.style }
						options={ [
							{ label: 'Default', value: 'default' },
							{ label: 'Card', value: 'card' },
							{ label: 'List', value: 'list' },
						] }
						onChange={ ( style ) => setAttributes( { style } ) }
					/>
				</PanelBody>
			</InspectorControls>

			<div { ...blockProps }>
				{/* Block content */}
			</div>
		</>
	);
}

Block Deprecation

deprecated array, save + attributes + migrate, apiVersion in entries, newest first:

❌ BAD: Save function changed without deprecation

// Changed save markup - existing blocks will show validation error
function save( { attributes } ) {
	const blockProps = useBlockProps.save();
	return <div { ...blockProps }>{ attributes.content }</div>;
}

✅ GOOD: Deprecation entry for old save function

import { useBlockProps } from '@wordpress/block-editor';

const deprecated = [
	{
		// Version 2: Added useBlockProps (newest deprecation first)
		apiVersion: 2,
		attributes: {
			content: { type: 'string' },
		},
		save( { attributes } ) {
			return <div className="my-block">{ attributes.content }</div>;
		},
	},
	{
		// Version 1: Original block
		apiVersion: 2,
		attributes: {
			content: { type: 'string' },
		},
		save( { attributes } ) {
			return <p>{ attributes.content }</p>;
		},
		migrate( attributes ) {
			// Transform old attributes to new schema
			return {
				...attributes,
				newField: 'default',
			};
		},
	},
];

// Current version
function save( { attributes } ) {
	const blockProps = useBlockProps.save();
	return <div { ...blockProps }>{ attributes.content }</div>;
}

export default {
	edit: Edit,
	save,
	deprecated,
};

Interactivity API

data-wp-interactive, data-wp-bind, data-wp-on, store(), wp_interactivity_state(), frontend-only emphasis:

❌ BAD: Custom JavaScript for frontend interaction

<?php
// render.php
?>
<div class="my-block">
	<button id="toggle-button">Toggle</button>
	<div id="content" style="display: none;">Hidden content</div>
</div>
<script>
	document.getElementById('toggle-button').addEventListener('click', function() {
		var content = document.getElementById('content');
		content.style.display = content.style.display === 'none' ? 'block' : 'none';
	});
</script>

✅ GOOD: Interactivity API with directives (WP 6.5+)

<?php
// render.php
defined( 'ABSPATH' ) || exit;

wp_interactivity_state( 'myPlugin', array(
	'isOpen' => false,
) );

$wrapper_attributes = get_block_wrapper_attributes();
?>
<div
	<?php echo $wrapper_attributes; ?>
	data-wp-interactive="myPlugin"
	data-wp-context='{ "id": "<?php echo esc_attr( uniqid() ); ?>" }'
>
	<button data-wp-on--click="actions.toggle">
		<?php esc_html_e( 'Toggle', 'my-plugin' ); ?>
	</button>
	<div data-wp-bind--hidden="!state.isOpen">
		<p><?php esc_html_e( 'Hidden content', 'my-plugin' ); ?></p>
	</div>
</div>
// view.js - Frontend store (NOT editor code)
import { store } from '@wordpress/interactivity';

store( 'myPlugin', {
	state: {
		isOpen: false,
	},
	actions: {
		toggle: ( { state } ) => {
			state.isOpen = ! state.isOpen;
		},
	},
} );

block.json configuration:

{
	"viewScriptModule": "file:./view.js"
}

Note: Interactivity API is for FRONTEND only. Editor still uses React (edit.js).

Import Patterns

@wordpress/* package imports vs window.wp.* legacy access:

❌ BAD: window.wp. global access (legacy)*

const { registerBlockType } = window.wp.blocks;
const { useBlockProps } = window.wp.blockEditor;
const { __ } = window.wp.i18n;

✅ GOOD: @wordpress/ package imports (modern)*

import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';

Block Supports

color, spacing, typography, align, anchor in block.json:

❌ BAD: Missing supports field

{
	"name": "my-plugin/my-block",
	"title": "My Block"
}

✅ GOOD: Complete supports configuration

{
	"name": "my-plugin/my-block",
	"title": "My Block",
	"supports": {
		"html": false,
		"color": {
			"background": true,
			"text": true,
			"gradients": true,
			"link": true
		},
		"spacing": {
			"margin": true,
			"padding": true,
			"blockGap": true
		},
		"typography": {
			"fontSize": true,
			"lineHeight": true,
			"fontFamily": true,
			"fontWeight": true
		},
		"align": [ "wide", "full" ],
		"anchor": true
	}
}

Block Variations

variation objects with attributes and innerBlocks:

import { registerBlockVariation } from '@wordpress/blocks';

registerBlockVariation( 'core/columns', {
	name: 'three-columns-equal',
	title: 'Three Columns (Equal)',
	description: 'Three columns with equal width',
	icon: 'columns',
	isDefault: false,
	scope: [ 'block' ],
	attributes: {
		columns: 3,
	},
	innerBlocks: [
		[ 'core/column' ],
		[ 'core/column' ],
		[ 'core/column' ],
	],
} );

Block Transforms

from/to patterns:

import { createBlock } from '@wordpress/blocks';

export default {
	edit: Edit,
	save,
	transforms: {
		from: [
			{
				type: 'block',
				blocks: [ 'core/paragraph' ],
				transform: ( { content } ) => {
					return createBlock( 'my-plugin/my-block', {
						content,
					} );
				},
			},
		],
		to: [
			{
				type: 'block',
				blocks: [ 'core/paragraph' ],
				transform: ( { content } ) => {
					return createBlock( 'core/paragraph', {
						content,
					} );
				},
			},
		],
	},
};

Block Context

providesContext/usesContext:

// Parent block.json
{
	"providesContext": {
		"myPlugin/userId": "userId",
		"myPlugin/userName": "userName"
	},
	"attributes": {
		"userId": { "type": "number" },
		"userName": { "type": "string" }
	}
}
// Child block.json
{
	"usesContext": [ "myPlugin/userId", "myPlugin/userName" ]
}
// Child edit.js
export default function Edit( { context } ) {
	const userId = context['myPlugin/userId'];
	const userName = context['myPlugin/userName'];

	return (
		<div>
			<p>User: { userName } ({ userId })</p>
		</div>
	);
}

Block Hooks

blockHooks with dynamic blocks only:

❌ BAD: Block hooks with static block

{
	"blockHooks": {
		"core/post-content": "after"
	}
}
// save.js - Static save = block hooks won't work
function save() {
	return <div>Content</div>;
}

✅ GOOD: Block hooks with dynamic block

{
	"blockHooks": {
		"core/post-content": "after"
	},
	"render": "file:./render.php"
}
// save.js - Dynamic block (null save) = block hooks work
function save() {
	return null;
}

@wordpress/scripts

package.json scripts configuration:

❌ BAD: Missing build scripts

{
	"scripts": {
		"test": "echo \"No build toolchain configured\""
	}
}

✅ GOOD: Complete @wordpress/scripts configuration

{
	"scripts": {
		"build": "wp-scripts build",
		"start": "wp-scripts start",
		"lint:js": "wp-scripts lint-js",
		"format": "wp-scripts format",
		"packages-update": "wp-scripts packages-update"
	},
	"devDependencies": {
		"@wordpress/scripts": "^27.0.0"
	}
}

Severity Definitions (BLK-24)

SeverityDefinitionExamples
CRITICALBlock won't render OR crashes editor OR causes block validation errorMissing apiVersion or using apiVersion 1/2 without migration path, invalid attribute type causing save validation error, save function mismatch without deprecation entry, missing useBlockProps.save() in apiVersion 3 block, InnerBlocks in edit but save returns null (not InnerBlocks.Content), wp_kses_post() on InnerBlocks $content parameter, window.wp.* access breaking modern build, missing register_block_type() call, deprecated source:'meta' attribute pattern
WARNINGBlock works but has quality/compatibility issues OR non-standard patternsDeprecated API usage (apiVersion 1/2 without critical issues), missing block supports (color, spacing, typography), no deprecation handler for changed save function, hardcoded strings without i18n in JS/JSX or PHP, multiple separate useSelect calls (performance issue), missing get_block_wrapper_attributes() in render.php, missing defined( 'ABSPATH' ) in PHP files, save function with side effects (Math.random, Date.now)
INFOBest practice improvements OR optimization opportunitiesCould use render PHP file instead of render_callback function, missing viewScript/viewScriptModule for frontend interactions, not using block.json $schema field, apiVersion 2 (upgrade to 3 available), missing keywords in block.json, block hooks opportunity for auto-insertion, could add block variations for common presets, missing template lock on fixed InnerBlocks layouts

Output Format (BLK-24)

Report findings grouped by FILE (PHP and JS/JSX files intermixed by actual file path), with line numbers and severity labels. Use BAD/GOOD code pairs for each finding.

# WordPress Block Review: my-block-plugin

## FILE: src/index.js

### Line 3: WARNING - Legacy global access
window.wp.* access is legacy pattern. Use @wordpress/* package imports for modern build toolchain.

❌ **BAD:**
```javascript
const { registerBlockType } = window.wp.blocks;

GOOD:

import { registerBlockType } from '@wordpress/blocks';

FILE: src/edit.js

Line 12: CRITICAL - Missing useBlockProps

useBlockProps() not called in edit function. Block won't render correctly in editor.

BAD:

export default function Edit() {
	return <div className="my-block">Content</div>;
}

GOOD:

import { useBlockProps } from '@wordpress/block-editor';

export default function Edit() {
	const blockProps = useBlockProps();
	return <div { ...blockProps }>Content</div>;
}

FILE: src/save.js

Line 5: CRITICAL - Missing useBlockProps.save()

apiVersion 3 blocks require useBlockProps.save(). Block validation errors will occur.

BAD:

function save() {
	return <div className="my-block">Content</div>;
}

GOOD:

import { useBlockProps } from '@wordpress/block-editor';

function save() {
	const blockProps = useBlockProps.save();
	return <div { ...blockProps }>Content</div>;
}

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 echo wp_kses_post( $content ); ?>

GOOD:

<?php echo $content; // InnerBlocks already sanitized ?>

Line 1: WARNING - Missing ABSPATH check

Add ABSPATH check at top of file to prevent direct access.

BAD:

<?php
$wrapper_attributes = get_block_wrapper_attributes();

GOOD:

<?php
defined( 'ABSPATH' ) || exit;

$wrapper_attributes = get_block_wrapper_attributes();

FILE: block.json

Line 2: WARNING - apiVersion 2 (upgrade available)

apiVersion 2 still works but upgrade to 3 recommended for iframe editor isolation (WP 6.3+).

BAD:

{
	"apiVersion": 2
}

GOOD:

{
	"apiVersion": 3
}

Line 10: INFO - Missing $schema field

Add $schema field for IDE validation support.

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	...
}

SUMMARY

Total issues: 7

  • CRITICAL: 3 (must fix - block won't render correctly)
  • WARNING: 3 (quality/compatibility issues)
  • INFO: 1 (best practice improvement)

Block validation risk: HIGH - Missing useBlockProps.save() will cause validation errors Security note: Missing ABSPATH check detected. Run /wp-sec-review for comprehensive security analysis. Plugin note: Block registration detected. Run /wp-plugin-review for plugin architecture review.


## Common Mistakes (BLK-25)

Patterns that look like issues but are NOT problems:

| Pattern | Why It's NOT a Problem | Context |
|---------|------------------------|---------|
| **Dynamic block save() returning null** | Server-side PHP renders output. save() doesn't need to return markup. | Correct for render_callback or render file usage |
| **InnerBlocks.Content without useBlockProps wrapper** | Valid for dynamic blocks that only need to persist nested blocks. | Dynamic blocks can return bare InnerBlocks.Content in save() |
| **viewScript not needed** | Block has no frontend JavaScript behavior. | Editor-only blocks or static HTML blocks don't need viewScript |
| **render_callback without return** | Valid if callback echoes output (though return is preferred). | WordPress handles both echo and return patterns |
| **Missing style/editorStyle in block.json** | Block uses theme defaults or has no custom styling. | Not all blocks need custom styles |
| **apiVersion 2 blocks** | Still work in WordPress 6.x+, just missing iframe isolation features. | Upgrade recommended but not critical |
| **Block without supports** | Valid for simple blocks that don't need color/spacing/typography. | Intentional for minimal blocks |
| **save() function with className in wrapper** | apiVersion 2 auto-injects classes. apiVersion 3 requires useBlockProps. | apiVersion-dependent behavior, not a bug |
| **window.wp.* in browser console** | Expected for debugging. Only @wordpress/* imports in source code matter. | Console access fine, source imports should use packages |
| **Interactivity API not used** | Blocks with no frontend interaction don't need Interactivity API. | Editor-only or static blocks work without frontend JS |
| **No block.json deprecation for attribute additions** | Adding new optional attributes doesn't break existing blocks. | Only save() markup changes need deprecation |
| **InnerBlocks without template** | Free-form nesting is valid. Template is optional. | Intentional when users should choose any blocks |

## Version Compatibility Reference

Quick reference for WordPress version requirements:

| Feature | WordPress Version | Notes |
|---------|-------------------|-------|
| block.json metadata support | 5.8+ | Single source of truth for block registration |
| apiVersion 2 | 5.6+ | Automatic wrapper class injection in save() |
| apiVersion 3 | 6.3+ | Iframe editor isolation, manual wrapper handling required |
| InspectorControls group prop | 6.2+ | Settings/Appearance tabs in sidebar |
| Block Hooks API | 6.4+ | Auto-insertion, dynamic blocks only |
| Interactivity API | 6.5+ | data-wp-* directives, viewScriptModule |
| viewScriptModule field | 6.5+ | ES module support for Interactivity API |
| Block Bindings API | 6.5+ | Dynamic attribute connections |
| useEntityProp hook | 5.9+ | Replacement for deprecated source:'meta' |

## Deep-Dive References

For advanced block development patterns, load these companion reference documents:

| Task | Reference to Load |
|------|-------------------|
| block.json schema validation, field-by-field reference, attribute type/source/selector patterns | `references/block-json-guide.md` |
| React/JSX editor component patterns, useBlockProps, RichText, InspectorControls, BlockControls, useSelect/useDispatch | `references/editor-patterns.md` |
| Server-side rendering, render_callback vs render file, get_block_wrapper_attributes, output escaping, $attributes/$content/$block parameters | `references/dynamic-blocks-guide.md` |
| Interactivity API directives (data-wp-*), wp_interactivity_state(), store() patterns, frontend state management | `references/interactivity-api-guide.md` |

**Note:** Reference docs provide deep-dive content. This SKILL.md is self-sufficient for standard block reviews.

**Security crossover:** When encountering security-relevant patterns (unescaped render output, user input in Interactivity API state, missing ABSPATH checks), this skill provides brief reminders but defers to wp-security-review for comprehensive security analysis. For detailed security patterns, use `/wp-sec-review` command.

**Plugin crossover:** When encountering plugin-level patterns (register_block_type() on init hook, ABSPATH checks, plugin headers), this skill provides brief mentions but defers to wp-plugin-development for plugin architecture depth. For detailed plugin architecture review, use `/wp-plugin-review` command.

ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ

詳細情報

作者
jorgerosal
リポジトリ
jorgerosal/wordpress-skills
ライセンス
MIT
最終更新
2026/4/17

Source: https://github.com/jorgerosal/wordpress-skills / ライセンス: MIT

本サイトは GitHub 上で公開されているオープンソースの SKILL.md ファイルをクロール・インデックス化したものです。 各スキルの著作権は原作者に帰属します。掲載に問題がある場合は info@alsel.co.jp または /takedown フォームよりご連絡ください。
原作者: jorgerosal · jorgerosal/wordpress-skills · ライセンス: MIT