Build a Custom Gutenberg Block with Dynamic Server-Side Rendering in WordPress

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.