Gutenberg blocks are the fundamental unit of WordPress content since WordPress 5.0 — building a custom block with its own edit and save functions gives full control over the editor experience and the front-end output without relying on shortcodes or widget hacks that predate the block editor. The @wordpress/create-block scaffold generates the complete directory structure, block.json, and build configuration for a new block — avoiding the boilerplate setup that slows down first-time block development. block.json is the authoritative metadata file for a registered block: it declares the block name, category, icon, attributes, and paths to editor and front-end script/style assets — WordPress reads this file via register_block_type() to handle all enqueueing automatically. Block attributes are the data model: each attribute has a type (string, boolean, array, object), a source (attribute, html, text, or meta), and a default value — they persist the block’s content between edit sessions. The edit function renders the block inside the Gutenberg editor, using React-like JSX and WordPress-provided components from @wordpress/block-editor and @wordpress/components. The save function renders the static HTML stored in the post content — it must be a pure function of the block’s attributes because WordPress validates the saved output against stored content on every load. Server-side rendering (render_callback in block.json) is an alternative to a static save function, ideal for blocks that display dynamic data such as recent posts or real-time product availability. Block variations, block styles, and block transforms extend a custom block’s flexibility without duplicating the block registration code. The custom post type post shows how to register the data model that a dynamic server-side-rendered block typically queries via WP_Query inside its render_callback. The REST API endpoint post covers the API pattern used when a block’s edit function needs to fetch live data from the server during editing. Run npm start during development for watch mode with hot-reloading — npm run build produces the production bundle with tree-shaking and minification before committing the compiled assets.
Problem: Adding structured, repeatable content patterns to WordPress via shortcodes or custom widgets produces brittle markup that breaks when themes change, lacks a native editor preview, and cannot integrate with the block editor's full-site editing templates and block patterns.
Solution: Scaffold a custom Gutenberg block with @wordpress/create-block, define typed attributes in block.json, build the edit component with InspectorControls for sidebar settings, and use a PHP render_callback for dynamic server-side output.
// 1. Scaffold the block plugin
// npx @wordpress/create-block my-callout-block --variant=dynamic
// cd my-callout-block && npm start
// 2. block.json — attributes and render mode
// {
// "name": "myplugin/callout",
// "title": "Callout",
// "category": "text",
// "icon": "megaphone",
// "attributes": {
// "message": { "type": "string", "default": "" },
// "type": { "type": "string", "default": "info" },
// "isDismissable": { "type": "boolean", "default": false }
// },
// "render": "file:./render.php"
// }
// 3. edit.js — block editor component
import { useBlockProps, InspectorControls, RichText } from '@wordpress/block-editor';
import { PanelBody, SelectControl, ToggleControl } from '@wordpress/components';
export default function Edit({ attributes, setAttributes }) {
const blockProps = useBlockProps({ className: `callout callout--${attributes.type}` });
return (
<>
<InspectorControls>
<PanelBody title="Callout settings">
<SelectControl
label="Type"
value={attributes.type}
options={[
{ label: 'Info', value: 'info' },
{ label: 'Warning', value: 'warning' },
{ label: 'Success', value: 'success' },
]}
onChange={(type) => setAttributes({ type })}
/>
<ToggleControl
label="Dismissable"
checked={attributes.isDismissable}
onChange={(isDismissable) => setAttributes({ isDismissable })}
/>
</PanelBody>
</InspectorControls>
<div {...blockProps}>
<RichText
tagName="p"
value={attributes.message}
onChange={(message) => setAttributes({ message })}
placeholder="Enter callout message…"
/>
</div>
</>
);
}
NOTE: The render.php file receives $attributes, $content, and $block as variables — always sanitize attribute values before outputting them: esc_html($attributes['message']) for plain text and wp_kses_post($attributes['message']) for content that may contain inline HTML from a RichText component.