Contents
Introduction
This tutorial explains, in complete detail, how to create Gutenberg blocks for WordPress using the official @wordpress/scripts toolchain. It covers project setup, block metadata, JavaScript implementation (editor and save), styling, build tooling, PHP registration, dynamic/server-side rendering, attributes, transforms, deprecations and migrations, internationalization, linting, testing, TypeScript considerations, and debugging tips. Code examples are included for every important part the code blocks use the required
wrapper so you can copy-paste directly.Prerequisites
- WordPress development environment (local WP site with WP 5.0 Gutenberg progressively integrated recommended latest WP).
- Node.js (LTS recommended) and npm (or yarn).
- Basic familiarity with ESNext / React-like JSX and WordPress block concepts (attributes, edit, save).
Two common starting routes
- Quick scaffold: Use npx @wordpress/create-block to scaffold a complete plugin. This is fast and sets up build, tests, translations and metadata. Example usage follows.
- Manual build with @wordpress/scripts: Create a plugin folder, add block metadata and source in a src/ folder, and use @wordpress/scripts for compiling and bundling. This tutorial focuses on the manual method but also shows the scaffold approach.
Quick scaffold (recommended to learn structure)
Run the official block creator to produce a ready-to-use plugin with @wordpress/scripts already configured:
npx @wordpress/create-block my-plugin-block
The generator asks a few questions and creates a plugin folder with src/, build tools, PHP registration and package.json scripts. Use it if you want a complete, best-practice baseline.
Manual setup using @wordpress/scripts (step-by-step)
1) Plugin folder and minimal plugin PHP file
Create a plugin folder in wp-content/plugins/your-block and a main PHP file. The PHP file will register block metadata and enqueue assets built by the JS build step.
The function register_block_type_from_metadata will read block.json in each block folder and automatically register scripts/styles built by the bundler if the expected asset files exist.
2) Initialize npm and install @wordpress/scripts
In the plugin root run:
npm init -y npm install --save-dev @wordpress/scriptsIf you want SCSS support, also install sass as a devDependency:
npm install --save-dev sass3) package.json scripts
Add scripts to package.json for development and production builds. Minimal recommended scripts:
{ name: my-custom-blocks, version: 1.0.0, private: true, devDependencies: { @wordpress/scripts: ^40.0.0, sass: ^1.32.0 }, scripts: { start: wp-scripts start, build: wp-scripts build, format: wp-scripts format, lint:js: wp-scripts lint-js, lint:css: wp-scripts lint-style, test: wp-scripts test-unit-js } }Use npm run start during development: it runs a dev server with watch rebuild and provides error overlays. Use npm run build when packaging for production.
4) Directory structure (recommended)
- plugin-folder/
- block-name/
- block.json
- src/
- index.js
- edit.js
- save.js
- editor.scss
- style.scss
- build/ (generated)
- my-custom-blocks.php (main plugin file)
- package.json
block.json (block metadata)
Using block.json is the recommended modern approach. register_block_type_from_metadata will use information here to register editor and frontend assets. Example:
{ schema: https://schemas.wp.org/trunk/block.json, apiVersion: 2, name: my-plugin/notice, title: Notice, category: widgets, icon: megaphone, description: A simple notice block with title and content., keywords: [alert, notice], version: 1.0.0, attributes: { title: { type: string, source: html, selector: .notice__title }, content: { type: string, source: html, selector: .notice__content }, isImportant: { type: boolean, default: false } }, supports: { html: false, anchor: true, align: [wide,full] }, textdomain: my-custom-blocks, editorScript: file:./build/index.js, editorStyle: file:./build/editor.css, style: file:./build/style.css }Notes:
- apiVersion 2 enables block API v2 (recommended).
- Use editorScript/style to point to the build outputs. register_block_type_from_metadata expects the build assets to exist at these paths/handles or will auto-generate handles if the files are present.
- Attributes with source and selector are retrieved from saved markup. You can also store attributes in comment attributes or as plain attributes.
Writing the block JavaScript
Use the src/index.js as the entry. Typical patterns:
- import edit and save implementations
- import styles
- register the block if youre not using server PHP register with block.json (or simply export to match block.json names)
Example: src/index.js (using registerBlockType directly)
import { registerBlockType } from @wordpress/blocks import Edit from ./edit import Save from ./save import ./editor.scss import ./style.scss registerBlockType( my-plugin/notice, { apiVersion: 2, edit: Edit, save: Save } )
If you use block.json and PHP register_block_type_from_metadata, you typically only need to export the editor script that calls registerBlockType with the same name in block.json.
Example: src/edit.js (editor UI with InspectorControls)
import { __ } from @wordpress/i18n import { useBlockProps, RichText, InspectorControls } from @wordpress/block-editor import { PanelBody, ToggleControl } from @wordpress/components import { Fragment } from @wordpress/element export default function Edit( { attributes, setAttributes } ) { const { title, content, isImportant } = attributes const blockProps = useBlockProps( { className: isImportant ? notice notice--important : notice } ) return () } setAttributes( { isImportant: value } ) } /> setAttributes( { title: value } ) } /> setAttributes( { content: value } ) } />
Example: src/save.js (static save)
import { useBlockProps, RichText } from @wordpress/block-editor export default function Save( { attributes } ) { const { title, content, isImportant } = attributes const blockProps = useBlockProps.save( { className: isImportant ? notice notice--important : notice } ) return () }
Styling the block
Create editor-specific and front-end styles. Typical files: src/editor.scss and src/style.scss. Import them from index.js so @wordpress/scripts processes them.
/ editor.scss / .notice { border-left: 4px solid #999 padding: 12px } .notice--important { border-color: #d43535 } / style.scss (frontend) / .notice__title { font-weight: bold margin-bottom: 8px } .notice__content { color: #333 }
Build output and asset files
When you run wp-scripts build, the default behavior is to take src/ as an entry and produce build/ files. If you maintain your own entry names, ensure block.json points to the right built files. Typical build outputs are:
- build/index.js — the compiled editor script
- build/style.css — front-end styles
- build/editor.css — editor styles
PHP registration via block.json (best practice)
With block.json in the block folder and the files above present, the PHP function register_block_type_from_metadata( __DIR__ ) in the plugin main file will register the block and enqueue the styles and scripts automatically. Example PHP (already shown earlier):
function my_custom_blocks_register() { register_block_type_from_metadata( __DIR__ ) } add_action( init, my_custom_blocks_register )
If you need a dynamic block (rendered on server), use the render_callback in register_block_type or add in block.json by omitting save and providing server-side code. Example:
Dynamic block: PHP render_callback example
function my_notice_render( attributes ) { title = isset( attributes[title] ) ? attributes[title] : content = isset( attributes[content] ) ? attributes[content] : class = ! empty( attributes[isImportant] ) ? notice notice--important : notice ob_start() ?>>my_notice_render, ) )
Using ServerSideRender in the editor
For dynamic blocks, in the editor you can show the server-rendered markup for a realistic preview:
import ServerSideRender from @wordpress/server-side-render export default function Edit( props ) { const { attributes, setAttributes } = props return () }
Attributes: types and sources
Attributes can be stored in multiple ways:
- source: html — pulls inner HTML from a selector (commonly used with RichText).
- source: attribute — reads an HTML attribute on a selector (useful for data- attributes or img src).
- type — string, boolean, number, object, array.
- default — default value when attribute missing.
Example attribute that stores image URL using attribute source:
attributes: { imageUrl: { type: string, source: attribute, attribute: src, selector: img } }
Transforms and variations
Transforms let you convert other blocks into this block and vice versa. Variations are predefined presets shown in the block inserter. Basic examples:
// Inside registerBlockType or block.json for variations variations: [ { name: warning, title: Warning, attributes: { isImportant: true } }, { name: info, title: Info, attributes: { isImportant: false } } ]
Deprecations and migrations
When you change saved markup or attributes, use the deprecated array on registerBlockType so older posts continue to render. Provide a migrate function to convert old attributes to new ones.
deprecated: [ { attributes: { oldAttr: { type: string } }, save: ( props ) => { // old save implementation }, migrate: ( attributes ) => { return { newAttr: attributes.oldAttr ? attributes.oldAttr : } } } ]
Internationalization
In JS use __ and related functions from @wordpress/i18n. In PHP load the textdomain in the plugin and register strings for translation. Example in JS:
import { __ } from @wordpress/i18n const label = __( Mark as important, my-custom-blocks )
In PHP load translations:
function my_custom_blocks_load_textdomain() { load_plugin_textdomain( my-custom-blocks, false, dirname( plugin_basename( __FILE__ ) ) . /languages ) } add_action( init, my_custom_blocks_load_textdomain )
Testing and linting
@wordpress/scripts ships testing and linting commands. You can run:
npm run lint:js npm run lint:css npm run test
Unit tests use Jest and the WordPress test environment. Example simple test:
// src/__tests__/index.test.js import { registerBlockType } from @wordpress/blocks jest.mock( @wordpress/blocks, () => ( { registerBlockType: jest.fn() } ) ) test( registers block, () => { require( ../index ) // index should register a block expect( registerBlockType ).toHaveBeenCalled() } )
TypeScript considerations
TypeScript can be used, but requires additional configuration. In many projects people:
- Rename files to .ts/.tsx.
- Add a tsconfig.json.
- Install type definitions like @types/react and types for WordPress packages if needed.
- Optionally extend or override wp-scripts webpack/babel behavior with a custom config (advanced).
For full TypeScript integration consult the official docs or use community starters that wire up tsconfig and types. A simple approach is to keep most logic in plain JS and add TypeScript gradually.
Developer workflow and debugging
- Use npm run start to watch changes and produce sourcemaps for easier debugging in the browser.
- Open the editor and watch console for build errors. wp-scripts overlays errors in the browser.
- Use the WordPress block inspector, and the browser devtools to inspect rendered DOM and block comment delimiters in post content.
- Use window.wp.data and the block editor store (wp.data.select(core/block-editor)) for runtime inspection, but use it only for debugging—do not depend on private APIs in production code.
Advanced topics
Enqueuing additional editor-only scripts
If you need editor-only utilities, declare them as editorScript or editorStyle in block.json. register_block_type_from_metadata will map file: entries to handles and enqueue them automatically.
REST API and dynamic content
Dynamic blocks that include content pulled from the REST API can render in the editor using ServerSideRender or client-side fetch for advanced interactive editors. For heavy dynamic UIs, consider decoupling editor UI (React) from server render and return simple markup from render_callback.
Tips for accessible markup
- Use semantic HTML (headings, paragraphs).
- Ensure controls are reachable by keyboard and labels are clear.
- Use appropriate ARIA roles when necessary.
Common pitfalls and solutions
- Block not appearing in inserter: Check block.json name and category ensure build assets exist and register_block_type_from_metadata is run.
- Attributes not persisted: Ensure save() returns markup matching attribute selectors and sources in block.json, or use save: null and a render_callback for dynamic server-side blocks.
- Script references 404: Confirm build created the referenced file names and that PHP register code points to the plugin directory path.
Complete minimal example: plugin with one block
File layout and contents compact example. Main plugin file (my-custom-blocks.php):
block-name/block.json (simple metadata):
{ schema: https://schemas.wp.org/trunk/block.json, apiVersion: 2, name: my-custom-blocks/simple, title: Simple Block, category: text, editorScript: file:./build/index.js, editorStyle: file:./build/editor.css, style: file:./build/style.css, attributes: { content: { type: string, source: html, selector: p } } }src/index.js:
import { registerBlockType } from @wordpress/blocks import Edit from ./edit import Save from ./save import ./editor.scss import ./style.scss registerBlockType( my-custom-blocks/simple, { apiVersion: 2, edit: Edit, save: Save } )src/edit.js:
import { useBlockProps, RichText } from @wordpress/block-editor import { __ } from @wordpress/i18n export default function Edit( { attributes, setAttributes } ) { const blockProps = useBlockProps() return () }setAttributes( { content: value } ) } placeholder={ __( Write your text…, my-custom-blocks ) } /> src/save.js:
import { useBlockProps, RichText } from @wordpress/block-editor export default function Save( { attributes } ) { return () }
References and further reading
- Block Editor Handbook
- @wordpress/scripts reference
- @wordpress/scripts source on GitHub
- Block metadata (block.json) reference
Checklist before shipping
- Run npm run build and confirm build files exist in block folder.
- Confirm register_block_type_from_metadata registers block without warnings.
- Test saving and editing the block, and verify older posts still render (handle deprecations if markup changed).
- Run lint and tests: npm run lint:js, npm run lint:css, npm run test.
- Prepare translations and load_plugin_textdomain in PHP.
- Bundle plugin and verify it installs and activates cleanly on a fresh WP install.
Final notes
@wordpress/scripts drastically reduces bundling configuration and provides standardized build tools for WordPress block development. Start from a small block, use block.json for metadata, import styles from your JS entry so they get processed, and rely on register_block_type_from_metadata in PHP to simplify registration. Use the scaffold tool for immediate productivity, and move to manual setup as you gain needs for customization.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |