WP Block Development
When to use
Use this skill for block work such as:
-
creating a new block or updating an existing one
-
changing block.json (scripts/styles/supports/attributes/render)
-
fixing "block invalid / not saving / attributes not persisting"
-
adding dynamic rendering (render.php / render_callback )
-
block deprecations and migrations (deprecated versions)
-
build tooling (@wordpress/scripts , @wordpress/create-block )
-
block patterns and variations
Inputs required
-
Repo root and target (plugin vs theme vs full site).
-
The block name/namespace and where it lives (path to block.json if known).
-
Target WordPress version range (especially for viewScriptModule / apiVersion 3).
Procedure
- Triage and locate blocks
-
Search for existing block.json files
-
Search for register_block_type calls
-
Identify the block root (directory containing block.json )
- Create a new block (if needed)
If creating a new block, prefer scaffolding:
npx @wordpress/create-block@latest my-custom-block
For interactive blocks, use the interactive template.
Read:
- references/creating-blocks.md
- Ensure apiVersion 3 (WordPress 6.9+)
WordPress 6.9 enforces apiVersion: 3 in block.json schema. Blocks with apiVersion 2 or lower trigger console warnings when SCRIPT_DEBUG is enabled.
Why this matters:
-
WordPress 7.0 will run the post editor in an iframe regardless of block apiVersion
-
apiVersion 3 ensures your block works correctly inside the iframed editor (style isolation, viewport units, media queries)
Migration from apiVersion 2:
-
Update the apiVersion field in block.json to 3
-
Test in a local environment with the iframe editor enabled
-
Ensure any style handles are included in block.json (styles missing from the iframe won't apply)
-
Third-party scripts attached to a specific window may have scoping issues
{ "apiVersion": 3, "name": "my-plugin/my-block", "...": "..." }
Read:
- references/block-json.md
- Pick the right block model
-
Static block (markup saved into post content): implement save()
-
Dynamic block (server-rendered): use render in block.json and keep save() minimal or null
-
Interactive frontend: use viewScriptModule for modern module-based view scripts
viewScript vs viewScriptModule:
Property viewScript
viewScriptModule
Module type Classic script ES Module
Loading Synchronous Async/deferred
Use for Legacy/compatibility Interactivity API, modern JS
Dependencies Manual registration Import statements
{ "viewScript": "file:./view.js", "viewScriptModule": "file:./view.js" }
Prefer viewScriptModule for:
-
Interactivity API (@wordpress/interactivity )
-
Modern ES module imports
-
Better performance (deferred loading)
- Update block.json safely
For field-by-field guidance:
Read:
- references/block-json.md
Common pitfalls:
-
Changing name breaks compatibility (treat it as stable API)
-
Changing saved markup without adding deprecated causes "Invalid block"
-
Adding attributes without defining source/serialization causes "attribute not saving"
- Register the block (server-side preferred)
Prefer PHP registration using metadata for:
-
Dynamic rendering
-
Translations (wp_set_script_translations )
-
Conditional asset loading
Read:
- references/registration.md
- Implement edit/save/render patterns
Follow wrapper attribute best practices:
-
Editor: useBlockProps()
-
Static save: useBlockProps.save()
-
Dynamic render (PHP): get_block_wrapper_attributes()
Read:
- references/edit-save-render.md
- Inner blocks (block composition)
If your block is a container that nests other blocks:
-
Use useInnerBlocksProps() to integrate inner blocks with wrapper props
-
Keep migrations in mind if you change inner markup
Read:
- references/inner-blocks.md
- Block patterns and variations
-
Patterns: predefined block arrangements
-
Variations: different configurations of a single block
Read:
- references/patterns-variations.md
- Migrations and deprecations
If you change saved markup or attributes:
-
Add a deprecated entry (newest → oldest)
-
Provide save for old versions and optional migrate
Read:
- references/deprecations.md
Verification
-
Block appears in inserter and inserts successfully.
-
Saving + reloading does not create "Invalid block".
-
Frontend output matches expectations (static: saved markup; dynamic: server output).
-
Assets load where expected (editor vs frontend).
-
Run the repo's lint/build/tests.
Failure modes / debugging
-
"Invalid block content" after changes:
-
Missing deprecation entry, changed markup without migration
-
Block attributes not saving:
-
Missing source definition, wrong attribute type, serialization mismatch
-
Block not appearing in inserter:
-
Registration failed, wrong category, PHP fatal error
-
Styles not applying in editor:
-
Missing editorStyle in block.json, wrong asset path
Read:
- references/debugging.md
Escalation
For canonical detail, consult:
-
Block Editor Handbook
-
Block API Reference
-
Gutenberg Storybook