wp-blocks

SKILL.md

WP Blocks, Block Themes, and Interactivity API

Consolidated skill for WordPress Gutenberg block development, block theme creation, and the Interactivity API. Targets WordPress 6.9+ (PHP 7.2.24+).


Part 1: Block Development

1.1 block.json Metadata

Every block starts with a block.json file. WordPress 6.9 enforces apiVersion 3.

{
  "$schema": "https://schemas.wp.org/trunk/block.json",
  "apiVersion": 3,
  "name": "my-plugin/my-block",
  "version": "1.0.0",
  "title": "My Block",
  "category": "widgets",
  "icon": "smiley",
  "description": "A custom block.",
  "supports": {
    "html": false,
    "color": { "background": true, "text": true },
    "typography": { "fontSize": true },
    "spacing": { "margin": true, "padding": true }
  },
  "attributes": {
    "content": { "type": "string", "source": "html", "selector": "p" },
    "alignment": { "type": "string", "default": "none" }
  },
  "textdomain": "my-plugin",
  "editorScript": "file:./index.js",
  "editorStyle": "file:./index.css",
  "style": "file:./style-index.css",
  "render": "file:./render.php",
  "viewScriptModule": "file:./view.js"
}

Required fields: apiVersion, name, title.

Asset fields:

Field Loads in Purpose
editorScript Editor only Block registration and edit UI
editorStyle Editor only Editor-specific styles
script Both Shared JS (editor + frontend)
style Both Shared styles
viewScript Frontend only Classic frontend script
viewScriptModule Frontend only Module-based frontend script (ES)
viewStyle Frontend only Frontend-only styles
render Server PHP render file for dynamic blocks

apiVersion 3 migration: Set "apiVersion": 3, declare all style handles in block.json (missing handles will not load in iframed editor), test third-party scripts (window scoping differs). WordPress 7.0 will always use the iframe editor regardless of apiVersion.

1.2 Scaffolding

npx @wordpress/create-block my-block                    # Standard block
npx @wordpress/create-block my-block --variant dynamic  # Dynamic block with render.php
npx @wordpress/create-block my-block --template @wordpress/create-block-interactive-template  # Interactive

For manual setup: create block.json, register via register_block_type_from_metadata() in PHP, add editor JS and view assets.

1.3 Static vs Dynamic Rendering

Type When to use save() returns
Static Self-contained HTML, no server state dependency Full markup
Dynamic Server data (posts, user info, APIs), must stay current null (or minimal fallback)
  • Static: markup stored in DB; changing save() without deprecation causes "Invalid block" errors.
  • Dynamic: render via render.php or render_callback.

1.4 Wrapper Functions (Required)

Context Function
Editor (edit.js) useBlockProps()
Static save (save.js) useBlockProps.save()
Dynamic render (PHP) get_block_wrapper_attributes()

These inject classes, styles, and data attributes generated by block supports. Always spread on the outermost wrapper element.

1.5 Attributes and Serialization

Attributes persist via comment delimiter JSON (default), HTML source (source + selector), or context (parent blocks).

Source Description
(none) Stored in block comment delimiter
attribute Parsed from an HTML attribute (selector + attribute)
text Parsed from element text content
html Parsed from element inner HTML
query Extracts an array from repeated elements

Rules: Avoid deprecated meta source. Avoid brittle selectors. Never change saved HTML without a deprecated entry.

1.6 InnerBlocks

For container blocks that nest other blocks. Use useInnerBlocksProps() in edit, useInnerBlocksProps.save() in save. Only one InnerBlocks per block. Use templateLock intentionally (false | 'all' | 'insert' | 'contentOnly').

1.7 Deprecations

Critical: When you change save() output or attribute shapes, add a deprecation entry. Order newest first. Each entry needs save matching old output. migrate is optional for attribute transforms. Never change save() without a deprecation entry.

1.8 Registration (PHP)

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

For dynamic blocks, pass 'render_callback' as second arg.

Detailed code examples (edit/save patterns, InnerBlocks, block supports, variations, styles, deprecation migrations, PHP registration): see resources/block-development.md


Part 2: Block Themes

2.1 Theme Structure

my-theme/
  style.css          # Theme header (required)
  theme.json         # Global settings and styles
  templates/
    index.html       # Minimum required template
    single.html
    page.html
    archive.html
    404.html
  parts/
    header.html
    footer.html
  patterns/
    hero.php
  styles/
    dark.json        # Style variation

2.2 theme.json Structure

theme.json (version 3) defines global settings and styles. Key sections:

Section Purpose
settings.color.palette Custom color presets
settings.typography.* Font families, sizes, line height
settings.spacing.* Units, spacing size presets
settings.layout contentSize and wideSize
settings.border Border controls and radius presets
styles.color Global background/text colors
styles.typography Global font settings
styles.elements.* Element styles (link, heading, button, input)
styles.blocks.* Per-block style overrides
customTemplates Custom template definitions
templateParts Template part declarations (header/footer/etc)

Reference presets with var(--wp--preset--<type>--<slug>).

Style hierarchy: core defaults -> theme.json -> child theme -> user customizations. User customizations stored in DB can override theme.json edits.

2.3 Templates and Template Parts

  • Templates in templates/ use block markup in HTML files.
  • Template parts in parts/ (flat, no subdirectories).
  • Reference parts via <!-- wp:template-part {"slug":"header","area":"header"} /-->.

2.4 Patterns

Filesystem patterns in patterns/*.php are auto-registered. Use docblock headers: Title, Slug, Categories, Keywords, Block Types, Post Types, Viewport Width. Add Inserter: no to hide from inserter.

2.5 Style Variations

JSON files under styles/ override settings and styles. Once a user selects a variation, the choice is stored in the database.

2.6 WordPress 6.9 theme.json Additions

  • Form element styling: styles.elements.input and styles.elements.select (border, color, outline, shadow, spacing). Focus state not yet supported.
  • Border radius presets: settings.border.radiusSizes for visual selection.
  • Button pseudo-classes: :hover and :focus states for Button block directly in theme.json.

Detailed code examples (full theme.json, templates, template parts, patterns, style variations, template hierarchy): see resources/block-themes.md


Part 3: Interactivity API

3.1 Directives Reference

Directive Purpose
data-wp-interactive Declares an interactive region and namespace
data-wp-context Provides per-element context (JSON)
data-wp-on--{event} Attaches synchronous event handler
data-wp-on-async--{event} Attaches async event handler (preferred)
data-wp-bind--{attr} Binds a DOM attribute to state/context
data-wp-text Sets element text content from state/context
data-wp-class--{name} Toggles a CSS class based on state/context
data-wp-style--{prop} Sets an inline style property
data-wp-each Iterates over an array
data-wp-key Unique key for list items
data-wp-watch Runs a callback when dependencies change
data-wp-init Runs once when the element is first connected
data-wp-run Runs a callback on every render

3.2 Store Pattern

Define stores with store( namespace, { state, actions, callbacks } ). Use getContext() for per-element context, getElement() for the current DOM ref. Async actions use generator syntax (*fetchData() { yield ... }).

State vs context:

  • State is global, shared across all instances. Define with store().
  • Context is per-element, scoped to nearest data-wp-context ancestor.

3.3 Server-Side Rendering (Required)

  1. Set "supports": { "interactivity": true } in block.json.
  2. Initialize state in PHP with wp_interactivity_state( $ns, $state ).
  3. Output context with wp_interactivity_data_wp_context( $context ).
  4. For themes/plugins without block.json, wrap HTML in wp_interactivity_process_directives().

PHP helper functions:

Function Purpose
wp_interactivity_state( $ns, $state ) Initialize or get global state for a namespace
wp_interactivity_data_wp_context( $context ) Generate data-wp-context attribute string
wp_interactivity_get_context( $ns ) Get current context during directive processing
wp_interactivity_process_directives( $html ) Manually process directives (themes/plugins)
wp_interactivity_config( $ns, $config ) Set configuration data for a namespace

3.4 Hydration Rules

  • Client JS must produce markup matching server-rendered HTML.
  • Derived state in JS only (not PHP) causes layout shift (hidden not set server-side).
  • Ensure PHP and JS derived state logic matches exactly.

3.5 WordPress 6.9 Changes

  • data-wp-ignore is deprecated. Use conditional rendering or separate interactive regions.
  • Unique directive IDs: Use --- separator for multiple same-type directives on one element.
  • getServerState() / getServerContext() reset between client-side page transitions.
  • Router regions support attachTo for overlays (modals, pop-ups).
  • New TypeScript types: AsyncAction<ReturnType> and TypeYield<T>.

Detailed code examples (store definitions, SSR render.php, derived state closures, non-block usage, 6.9 directive IDs): see resources/interactivity-api.md


Part 4: Tooling

4.1 @wordpress/scripts

npx wp-scripts start          # Development build with watch
npx wp-scripts build          # Production build
npx wp-scripts lint-js        # Lint JS
npx wp-scripts lint-style     # Lint CSS
npx wp-scripts test-unit-js   # Unit tests
npx wp-scripts test-e2e       # E2E tests

4.2 wp-env

npx wp-env start              # Start environment
npx wp-env stop               # Stop environment
npx wp-env run cli wp plugin list  # Run WP-CLI commands
npx wp-scripts test-e2e       # Run E2E tests against the environment

4.3 Debugging Common Issues

"This block contains unexpected or invalid content":

  • You changed save() output or attribute parsing without a deprecation entry.
  • Fix: add a deprecated entry with the old save function and optionally a migrate function.

Block does not appear in inserter:

  • Confirm block.json name is valid and the block is registered.
  • Confirm build output exists and scripts are enqueued.
  • If using PHP registration, confirm register_block_type_from_metadata() runs on the init hook.

Attributes not saving:

  • Confirm attribute definition matches actual markup structure.
  • If the value is in comment delimiter JSON, avoid brittle HTML selectors.
  • Avoid the deprecated meta attribute source.

Styles not applying in editor (apiVersion 3):

  • Ensure style handles are declared in block.json (editorStyle, style).
  • Styles not declared in block.json will not load inside the iframed editor.

Console warnings about apiVersion (WordPress 6.9+):

  • Update apiVersion to 3 in block.json. The warning only appears when SCRIPT_DEBUG is true.

Interactivity directives not firing:

  • Confirm the viewScriptModule is enqueued and loaded (check network tab).
  • Confirm the DOM element has data-wp-interactive with the correct namespace.
  • Confirm the store namespace matches the directive values.
  • Check console for JS errors before hydration.
  • Confirm supports.interactivity is set in block.json (or wp_interactivity_process_directives() is called).

Hydration mismatch / flicker:

  • Server markup differs from client expectations.
  • Derived state not defined in PHP causes missing attributes on initial render.
  • Ensure PHP and JS derived state logic matches.

Part 5: Common Mistakes

Mistake Consequence Fix
Changing save() without deprecation "Invalid block" error on existing posts Add deprecated array entry with old save
Renaming block name in block.json All existing instances break Treat name as immutable stable API
Missing useBlockProps() in edit Block supports (colors, spacing) not applied Always spread useBlockProps() on wrapper
Missing useBlockProps.save() in save Support classes/styles missing from saved markup Always spread on outermost save element
Missing get_block_wrapper_attributes() in PHP render Support classes/styles missing from frontend Always use on wrapper in render.php
Using innerHTML = for block save XSS risk and bypasses sanitization Use proper React components and RichText.Content
Attribute source selector too brittle Attribute value not found after minor markup change Use stable selectors or prefer comment delimiter
apiVersion below 3 Console warnings in 6.9; broken in 7.0 iframe editor Set apiVersion: 3 and test in iframe
Derived state only in JS, not PHP Layout shift on initial load; hidden not set server-side Define matching derived state in PHP with wp_interactivity_state()
Not declaring styles in block.json Styles load on frontend but not in iframed editor Add all handles to editorStyle / style fields
Using data-wp-ignore Deprecated in 6.9; breaks context and navigation Use conditional rendering or separate regions
Template parts in subdirectories Parts not found by WordPress Keep parts flat in parts/ directory
User customizations overriding theme.json Theme changes appear ignored Check for DB-stored user overrides; reset if needed
Duplicate InnerBlocks in one block Runtime error Only one InnerBlocks per block
templateLock: 'all' without good reason Users cannot modify block content Use sparingly; prefer false or 'insert'

Resources

Detailed code examples and extended references are available in:

  • resources/block-development.md -- edit/save patterns, dynamic rendering, InnerBlocks composition, block supports (full JSON), block variations, block styles, deprecation migrations, PHP registration
  • resources/block-themes.md -- full theme.json example, template markup, template parts, pattern docblocks, style variations, template hierarchy
  • resources/interactivity-api.md -- store definitions, SSR render.php examples, derived state closures, non-block usage with wp_interactivity_process_directives(), PHP helper functions, WordPress 6.9 directive changes
Weekly Installs
6
GitHub Stars
3
First Seen
Feb 5, 2026
Installed on
opencode6
gemini-cli6
github-copilot6
codex6
amp6
kimi-cli6