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 (
deprecatedversions) - 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.jsonif known). - Target WordPress version range (especially for
viewScriptModule/ apiVersion 3).
Procedure
0) Triage and locate blocks
- Search for existing
block.jsonfiles - Search for
register_block_typecalls - Identify the block root (directory containing
block.json)
1) 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
2) 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
apiVersionfield inblock.jsonto3 - 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
windowmay have scoping issues
{
"apiVersion": 3,
"name": "my-plugin/my-block",
"...": "..."
}
Read:
references/block-json.md
3) Pick the right block model
- Static block (markup saved into post content): implement
save() - Dynamic block (server-rendered): use
renderinblock.jsonand keepsave()minimal ornull - Interactive frontend: use
viewScriptModulefor 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)
4) Update block.json safely
For field-by-field guidance:
Read:
references/block-json.md
Common pitfalls:
- Changing
namebreaks compatibility (treat it as stable API) - Changing saved markup without adding
deprecatedcauses "Invalid block" - Adding attributes without defining source/serialization causes "attribute not saving"
5) 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
6) 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
7) 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
8) Block patterns and variations
- Patterns: predefined block arrangements
- Variations: different configurations of a single block
Read:
references/patterns-variations.md
9) Migrations and deprecations
If you change saved markup or attributes:
- Add a
deprecatedentry (newest → oldest) - Provide
savefor old versions and optionalmigrate
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
sourcedefinition, wrong attribute type, serialization mismatch
- Missing
- Block not appearing in inserter:
- Registration failed, wrong category, PHP fatal error
- Styles not applying in editor:
- Missing
editorStylein block.json, wrong asset path
- Missing
Read:
references/debugging.md
Escalation
For canonical detail, consult: