Registering a Block

registerBlockType tells the editor how a block is edited and saved, and @wordpress/scripts compiles the JSX a browser cannot run.

SyntaxregisterBlockType( nameOrMetadata, { edit, save, ...settings } )

Registering in PHP makes a block exist on the server. The editor still needs to know how to draw and save it, and that lives in JavaScript: registerBlockType() from @wordpress/blocks, called from the file named in editorScript.

Two forms of the same call

// Longhand: the name, plus a settings object repeating the metadata.
registerBlockType( 'sitenotice/notice', {
    title: 'Notice',
    category: 'text',
    attributes: { message: { type: 'string' } },
    edit: Edit,
    save: Save,
} );

// Shorthand: import block.json and pass it as the first argument.
import metadata from './block.json';
registerBlockType( metadata, { edit: Edit, save: Save } );

Both register the same block. The second is preferred because the metadata is not written twice, and it is the form wp-scripts expects.

edit and save

edit is a React component rendered in the canvas; its props include attributes, setAttributes, isSelected and clientId. save returns the markup serialized into post_content. Wrap the outermost element with useBlockProps() in edit and useBlockProps.save() in save, both from @wordpress/block-editor; they contribute the generated class name and everything the supports settings produced.

The build step

Browsers cannot run JSX or resolve import ... from '@wordpress/blocks'. wp-scripts build scans src/ for block.json files, treats the assets they name as entry points, and writes build/index.js, build/index.css, build/style-index.css and build/index.asset.php. wp-scripts start is the same with a watcher.

Example

Example · php
add_action( 'init', 'sitenotice_register' );

function sitenotice_register(): void {
	$block_type = register_block_type( __DIR__ . '/build/notice' );

	if ( ! $block_type instanceof WP_Block_Type ) {
		return;
	}

	// The handle for the editor script is generated from block.json, so read it
	// back off the registered type rather than guessing its name.
	// editor_script_handles: since WordPress 6.1, and it is an array.
	foreach ( $block_type->editor_script_handles as $handle ) {
		// Makes __() calls inside the compiled JS translatable. Without this the
		// editor UI stays in English no matter what .json files you ship.
		wp_set_script_translations(
			$handle,
			'sitenotice',
			plugin_dir_path( __FILE__ ) . 'languages'
		);
	}
}

When to use it

  • Building a custom block for an editorial team instead of teaching them a shortcode
  • Setting up a repeatable build for a plugin that ships several blocks
  • Changing a shipped block's markup without invalidating existing posts

More examples

src/index.js for a static block

The whole JavaScript side of a static block. RichText in edit pairs with RichText.Content in save; the attribute in block.json for this uses source "html" with selector "p", so the text is read back out of the saved paragraph. Note the two imported stylesheets landing in different build files.

Example · javascript
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps, RichText } from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import metadata from './block.json';
import './style.scss';   // -> build/style-index.css (editor + front end)
import './editor.scss';  // -> build/index.css (editor only)

function Edit( { attributes, setAttributes } ) {
	const blockProps = useBlockProps( { className: `is-${ attributes.level }` } );

	return (
		<div { ...blockProps }>
			<RichText
				tagName="p"
				value={ attributes.message }
				onChange={ ( message ) => setAttributes( { message } ) }
				placeholder={ __( 'Write the notice…', 'sitenotice' ) }
			/>
		</div>
	);
}

function Save( { attributes } ) {
	// useBlockProps.save() must mirror useBlockProps() above, including the
	// extra className, or the saved markup will not match what edit rendered.
	const blockProps = useBlockProps.save( { className: `is-${ attributes.level }` } );

	return (
		<div { ...blockProps }>
			<RichText.Content tagName="p" value={ attributes.message } />
		</div>
	);
}

registerBlockType( metadata, { edit: Edit, save: Save } );

package.json for the build

@wordpress/scripts is the only dev dependency needed; it bundles webpack, Babel, ESLint and Prettier with WordPress defaults, and marks every @wordpress/* import as external so the compiled file uses the copies WordPress already loads. Run npm run start while developing and npm run build before tagging a release — shipping without a build is the single most common cause of "my block does not appear".

Example · json
{
  "name": "site-notice",
  "scripts": {
    "build": "wp-scripts build",
    "start": "wp-scripts start",
    "lint:js": "wp-scripts lint-js",
    "format": "wp-scripts format"
  },
  "devDependencies": {
    "@wordpress/scripts": "^30.0.0"
  }
}

Changing saved markup without breaking posts

When you must change save output on a block that is already in use, move the old save (and the attribute definitions it went with) into a deprecated entry rather than deleting it. The parser tries each deprecation in turn; a post saved with the old markup validates against v1 and is silently migrated on the next save. Without this, every existing post shows a block recovery prompt.

Example · javascript
import { useBlockProps, RichText } from '@wordpress/block-editor';

const v1 = {
	attributes: {
		message: { type: 'string', source: 'html', selector: 'p', default: '' },
		level: { type: 'string', default: 'info' },
	},
	save( { attributes } ) {
		// The markup this block used to produce, kept verbatim.
		return (
			<div { ...useBlockProps.save() }>
				<RichText.Content tagName="p" value={ attributes.message } />
			</div>
		);
	},
};

registerBlockType( metadata, {
	edit: Edit,
	save: Save,        // The new markup.
	deprecated: [ v1 ], // Old shapes, newest first.
} );

Discussion

  • Be the first to comment on this lesson.