Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A dynamic WordPress block generates its front-end HTML when WordPress serves a page instead of storing a permanent markup snapshot in the post. Scaffold one with npx @wordpress/create-block@latest my-dynamic-block --variant=dynamic, describe its attributes in block.json, render with render.php or a PHP render_callback, and return null from save() when the block has no editor-saved HTML. This keeps data current and lets markup or styles change for existing instances without mass-editing posts.

What a dynamic block is and when to use one

A Gutenberg block is dynamic when its front-end markup is produced during page rendering. The post stores the block delimiter and its attributes; PHP then receives those values and builds the output for each request. A static block instead saves its generated HTML in post content and normally serves that saved snapshot until the post is edited.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Dynamic rendering is useful when the output depends on information that can change independently of the post. Examples include a list of the latest posts, a logged-in user’s state, inventory or availability supplied by WordPress, or markup that you need to improve across already-published pages. Updating the PHP template or callback changes future responses from every instance without opening and resaving each post.

Use a static block when the content is deliberately a historical snapshot, when it does not depend on server state, or when avoiding per-request PHP work is more important than freshness. Dynamic output adds server processing, so the decision should reflect how often the data changes and whether the block needs request-time context.

How the pieces fit together

A production block normally has four contracts:

  • Metadata: block.json declares the block name, API version, title, attributes, editor script and, for a PHP template, the render file.
  • Editor UI: edit.js displays controls and an editor representation. It receives the attributes defined in metadata and updates them through the block-editor props.
  • Saved content: save() determines whether WordPress stores generated HTML, nested block content, or only the block delimiter and attributes.
  • Server renderer: render.php or a registered render_callback turns attributes, saved content and the block instance into front-end markup.

The block name is a stable namespace/slug identifier. Choose it before publishing: changing it later creates a different block and can strand existing content. The scaffold command’s slug also determines the plugin folder and internal block name. Pairing the block with a plugin rather than a theme keeps it available when the site changes themes.

Scaffold the plugin

Install a current Node.js and npm environment suitable for the WordPress build tool, then run the official dynamic variant from the directory where you keep plugins:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
npx @wordpress/create-block@latest my-dynamic-block --variant=dynamic

The generator creates a plugin directory with PHP registration, JavaScript source, CSS, metadata and a build configuration. Activate the generated plugin in Plugins in the WordPress dashboard before testing the block. During development, run the command provided by the generated package (normally npm start) from that directory so source changes are rebuilt while you work. Use npm run build for optimized production assets.

Choose the namespace and slug deliberately

In the command above, my-dynamic-block is the slug. A namespace such as acme and a slug such as latest-posts produce the block name acme/latest-posts. Keep the name lowercase, unique to your plugin, and stable across releases. The name appears in block comments saved in posts, so a later rename requires a migration rather than a simple file rename.

Define the block contract in block.json

block.json is the shared metadata contract used by the editor and server registration. Its attributes object defines the values passed to the Edit component, save(), and server-side rendering. The render property can point to a PHP template; that property was introduced in WordPress 6.1.

{"apiVersion":3,"name":"acme/latest-posts","title":"Latest Posts","category":"widgets","attributes":{"count":{"type":"number","default":5}},"editorScript":"file:./index.js","render":"file:./render.php"}

This example stores one numeric attribute, count, defaulting to five. The editorScript value tells WordPress to load the built JavaScript file generated by the package. The render value makes the generated PHP file the front-end renderer.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Attribute design rules

  • Give each attribute an explicit type such as number, string or boolean.
  • Provide a default for optional controls so older instances have a predictable value.
  • Store data rather than derived HTML. A query limit, selected taxonomy ID or display toggle is easier to validate and migrate than a prebuilt markup string.
  • Treat attributes as untrusted input. Validate and normalize them again in PHP; editor controls do not protect requests that are sent directly to the server.

Build the editor interface

In edit.js, use WordPress block-editor components for controls and useBlockProps() for the editor wrapper. The editor can show a lightweight representation or a server-rendered preview; neither choice changes how the front end is generated.

import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl } from '@wordpress/components';

export default function Edit( { attributes, setAttributes } ) {
    const { count } = attributes;
    const blockProps = useBlockProps();

    return (
        <div { ...blockProps }>
            <InspectorControls>
                <PanelBody title={ __( 'Query', 'acme' ) }>
                    <RangeControl
                        label={ __( 'Number of posts', 'acme' ) }
                        value={ count }
                        onChange={ ( value ) => setAttributes( { count: value } ) }
                        min={ 1 }
                        max={ 20 }
                    />
                </PanelBody>
            </InspectorControls>
            <p>{ __( 'Latest posts will appear here.', 'acme' ) }</p>
        </div>
    );
}

The wrapper returned by useBlockProps() gives the editor the classes and data it needs. Keep the editor component focused on selecting attributes and communicating state; query and format the final list in PHP unless there is a compelling reason to duplicate that logic in JavaScript.

Render the front end with render.php

A template named by render receives three exposed variables: $attributes, $content and $block. Use the first for configured values, the second for saved nested content, and the third when you need information from the current block instance or context.

<?php
$count = isset( $attributes['count'] ) ? absint( $attributes['count'] ) : 5;
$count = min( max( $count, 1 ), 20 );
$posts = get_posts(
    array(
        'numberposts' => $count,
        'post_status' => 'publish',
    )
);
?>
<ul <?php echo get_block_wrapper_attributes(); ?>>
    <?php foreach ( $posts as $post ) : ?>
        <li>
            <a href="<?php echo esc_url( get_permalink( $post ) ); ?>">
                <?php echo esc_html( get_the_title( $post ) ); ?>
            </a>
        </li>
    <?php endforeach; ?>
</ul>

absint() converts the attribute to a non-negative integer, and the additional bounds prevent an unexpectedly large query. The query requests published posts only. get_permalink() is escaped for a URL, while the title is escaped as text. Use the escaping function that matches the output context rather than applying one generic function everywhere.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Preserve block supports with the wrapper helper

Call get_block_wrapper_attributes() on the outer element. It adds the classes and attributes generated by block supports, such as alignment or spacing, so the server-rendered markup remains consistent with the editor configuration. If you build the wrapper manually, supported features can silently stop applying on the front end.

Escape generated markup

Escape each value at the point where it is output. For a value that is intentionally generated as post-like HTML, the official dynamic-block tutorial demonstrates sanitizing the complete fragment with wp_kses_post( $block_content ). Do not echo raw attribute values, URLs or titles, and do not assume that an editor control makes a value safe.

Use a render_callback instead of a template when appropriate

A PHP callback is an alternative to the render file. Register the block with register_block_type() and pass a function or callable as render_callback. The callback receives the same rendering context: attributes, saved content and the block instance.

register_block_type(
    __DIR__ . '/build',
    array(
        'render_callback' => 'acme_render_latest_posts',
    )
);

function acme_render_latest_posts( $attributes, $content, $block ) {
    $count = isset( $attributes['count'] ) ? absint( $attributes['count'] ) : 5;
    $posts = get_posts(
        array(
            'numberposts' => min( max( $count, 1 ), 20 ),
            'post_status' => 'publish',
        )
    );

    $html = '<ul ' . get_block_wrapper_attributes() . '>';
    foreach ( $posts as $post ) {
        $html .= '<li><a href="' . esc_url( get_permalink( $post ) ) . '">';
        $html .= esc_html( get_the_title( $post ) );
        $html .= '</a></li>';
    }
    $html .= '</ul>';
    return $html;
}

Choose render.php when a template keeps presentation readable and close to the markup. Choose a callback when registration or shared PHP services make a function more maintainable. Do not register both render mechanisms for the same block; one should be the authoritative renderer.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Decide what save() should return

For a block that is entirely generated by PHP, return null from save():

export default function save() {
    return null;
}

This tells WordPress not to store a complete HTML snapshot. The post retains the block delimiter and attributes, and the renderer creates the front-end output later. Returning null also avoids validation failures when your generated markup changes between releases.

When the block contains InnerBlocks

If your block is a server-rendered container with nested blocks, preserve the nested content explicitly:

import { InnerBlocks } from '@wordpress/block-editor';

export default function save() {
    return <InnerBlocks.Content />;
}

The parent’s PHP renderer receives that saved nested markup in $content. Omitting <InnerBlocks.Content /> discards the child blocks from the saved post even though the editor displayed them.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When a fallback is useful

A dynamic block may save fallback HTML that can remain visible if its plugin is deactivated or its renderer is unavailable. This is a deliberate resilience choice, not a requirement. If the fallback is saved, ensure it reflects the same attributes and is safe to display; otherwise a null save is clearer and avoids stale markup.

Preview PHP output inside the editor

The editor cannot execute PHP directly. For a PHP-heavy block, @wordpress/server-side-render can request the registered renderer through the block-renderer REST endpoint, whose route has the form /wp/v2/block-renderer/:block. The response is then displayed as the preview.

import ServerSideRender from '@wordpress/server-side-render';

export default function Edit( { attributes } ) {
    return <ServerSideRender
        block="acme/latest-posts"
        attributes={ attributes }
    />;
}

Pass the same attribute object that the editor will save. Handle loading and error states so an unavailable endpoint does not make the editing surface unusable. The package documentation describes this mechanism as a fallback or legacy approach; for new features, prefer a REST endpoint and client-side rendering when practical. Use server-side render previews when the block is tightly coupled to PHP or when reproducing the exact server output in the editor is more valuable than a separate client implementation.

Dynamic and static blocks compared

Decision point Dynamic block Static block
Data freshness Reflects current server data when the page is requested. Reflects the saved markup until the post is updated.
Markup updates Changing the template or callback can update existing instances without editing posts. Changing generation logic does not rewrite old saved markup automatically.
Validation A null save avoids many saved-markup validation problems. Changed generated HTML can produce a validation warning.
Request cost Runs server-side rendering work on requests, subject to normal caching and query costs. Serves stored output with less rendering work.
Failure behavior A saved fallback can keep content visible if the renderer disappears; without one, output depends on the plugin and callback being available. Saved HTML can remain visible even when generation code is no longer active.
Best fit Frequently changing data, server context, or centrally maintained markup. Stable content where a snapshot and minimal request work are preferable.

Build and release workflow

  1. Run the scaffold command and activate the plugin in a development site.
  2. Define the final namespace, slug, attributes and render strategy in block.json.
  3. Use the generated development command while editing JavaScript and CSS.
  4. Test insertion, attribute changes, post saving, front-end rendering and reloading an older post.
  5. Run npm run build from the plugin directory to create optimized production assets.
  6. Deploy the built files and PHP together, then verify that the plugin is active before opening pages containing the block.

Keep metadata and PHP changes synchronized with the built JavaScript. A deployed block.json that names a file absent from the build directory can leave the block unavailable or produce an editor error.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common failure modes and fixes

The block shows a validation error

Check whether save() returns markup even though PHP now generates different output. For a fully dynamic block, return null and confirm that the block’s name and attributes have not changed. If you intentionally save fallback HTML, make sure the saved structure remains valid for the current implementation.

Nested blocks disappear

Return <InnerBlocks.Content /> from save() and use the renderer’s $content variable. A null save is correct only when no nested editor content must be retained.

Alignment or spacing settings do nothing

Inspect the outer element generated by PHP. It should include get_block_wrapper_attributes(); otherwise block-support classes and attributes may be missing even though the editor displays them.

The editor preview is blank or stale

Confirm that the block is registered on the server, that the block name passed to ServerSideRender exactly matches the metadata name, and that the attributes sent by the editor use the declared types. Inspect the block-renderer REST request for permission or PHP errors. If the preview depends on a large query, provide a lightweight editor placeholder or move to a REST-backed client preview.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Titles or links contain unsafe markup

Apply context-specific escaping in the renderer: esc_url() for URLs, esc_html() for plain text, and an appropriate HTML sanitizer for intentionally permitted markup. Never concatenate raw attributes into HTML or URL strings.

The block stops working after a theme change

Register it from a plugin and keep the plugin active. Theme registration couples availability to that theme and can make existing posts lose their block type when the site design changes.

Pages become slow

Review the query performed by the renderer, cap numeric attributes, request only the data needed for the markup, and consider caching where the data’s freshness requirements allow it. Dynamic does not mean uncached; it means the output is generated by the server rather than stored as post HTML.

Production checklist

  • Use a permanent, unique namespace/slug and keep it unchanged after publishing.
  • Register the block from a plugin when it should survive theme changes.
  • Declare every attribute and its type in block.json; provide safe defaults.
  • Choose either a render.php template or a render_callback.
  • Normalize and bound attributes again in PHP.
  • Use get_block_wrapper_attributes() on the front-end wrapper.
  • Escape every output value for its context.
  • Return null from save() when no HTML snapshot is needed.
  • Return <InnerBlocks.Content /> when nested blocks must be saved.
  • Use server-side preview only when its PHP coupling is justified; prefer a REST-backed client experience for new features where practical.
  • Run npm run build and deploy the resulting assets with the PHP files.

How to choose the architecture

Start with the data lifecycle. If a reader should see the latest server state on every request, use dynamic rendering. If the block is a fixed editorial composition, static output is simpler. For a dynamic block with child blocks, combine PHP rendering with InnerBlocks.Content. For a PHP-only block, a null save and a focused renderer minimize validation friction. Finally, decide whether the editor needs exact PHP output: use ServerSideRender for legacy or tightly coupled cases, and design a REST-backed client preview for new experiences when that separation is practical.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.