--- name: wp-blocks description: Use when building Gutenberg blocks, block themes, or using the Interactivity API. Covers block.json configuration, register_block_type, @wordpress/scripts tooling, useBlockProps, RichText, InspectorControls, BlockControls, wp-block-editor packages, static and dynamic rendering, InnerBlocks, deprecations, theme.json, FSE (full site editing), template parts, block patterns, block variations, data-wp-* directives, server-side rendering, and WordPress 6.9 features. --- # 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**. ```json { "$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 ```bash 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) ```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----)`. **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 ``. ### 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` and `TypeYield`. > **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 ```bash 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 ```bash 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