Dynamic Blocks

Dynamic blocks store attributes only and build their markup in PHP on every request, through render.php or a render_callback.

Syntaxfunction my_render( array $attributes, string $content, WP_Block $block ): string

A static block writes finished HTML into post_content. A dynamic block writes only its attributes and produces markup in PHP each time the post is rendered. Anything that must be correct at read time — recent posts, a stock level, the current user's name, a count — has to be dynamic, because a static block freezes the value at the moment the author pressed Update.

Two ways to attach the PHP

// Shorthand, in block.json (WordPress 6.1):
"render": "file:./render.php"

// Longhand, in PHP:
register_block_type( __DIR__ . '/build/latest', array(
    'render_callback' => 'sitekit_render_latest',
) );

They reach the same place. render.php is included with $attributes (array), $content (the saved inner markup, usually empty) and $block (a WP_Block) already in scope, and whatever it prints is the output. A render_callback receives those same three values as arguments and must return the string rather than echo it.

The wrapper element

Call get_block_wrapper_attributes() (WordPress 5.6) on the outermost element. It produces the block's own class plus every class and inline style the supports settings generated — colour, spacing, typography, the custom class name. Note that the anchor support has no server-side handler in WordPress 6.8, so a dynamic block's id is not emitted for you; output it from $attributes['anchor'] yourself. Build the class attribute by hand instead and the user's editor choices produce nothing on the front end.

The trade

The block's save returns null, so only the self-closing delimiter is stored. Nothing saved means no validation errors ever, and a PHP change updates every existing post at once. It also means the work runs on every request.

Example

Example · php
<?php
/**
 * render.php for sitekit/latest-posts.
 *
 * Included by WordPress with these already in scope — do not declare them.
 *
 * @var array    $attributes Validated against the schema in block.json.
 * @var string   $content    Saved inner markup (empty for this block).
 * @var WP_Block $block      The block instance, including its context.
 */

$posts = get_posts(
	array(
		'numberposts'      => max( 1, min( 20, (int) ( $attributes['count'] ?? 3 ) ) ),
		'post_status'      => 'publish',
		'ignore_sticky_posts' => true,
	)
);

if ( ! $posts ) {
	return; // Returning early from an included file simply prints nothing.
}
?>
<ul <?php echo get_block_wrapper_attributes(); // Already escaped by core. ?>>
	<?php foreach ( $posts as $post ) : ?>
		<li>
			<a href="<?php echo esc_url( (string) get_permalink( $post ) ); ?>">
				<?php echo esc_html( get_the_title( $post ) ); ?>
			</a>
			<?php if ( ! empty( $attributes['showDate'] ) ) : ?>
				<time datetime="<?php echo esc_attr( get_the_date( 'c', $post ) ); ?>">
					<?php echo esc_html( get_the_date( '', $post ) ); ?>
				</time>
			<?php endif; ?>
		</li>
	<?php endforeach; ?>
</ul>

When to use it

  • Showing the latest posts, products or events inside page content
  • Rendering something that depends on the current user or request
  • Changing the markup of a shipped block across a whole site without touching any post

More examples

The JavaScript half of a dynamic block

ServerSideRender (from @wordpress/server-side-render, listed as a dependency in package.json) round-trips to the server on every attribute change, so it is fine for a settings-driven block and poor for anything the user types into directly. For a block with rich inline editing, write a JS preview in edit and keep the PHP for the front end.

Example · javascript
import { registerBlockType } from '@wordpress/blocks';
import { useBlockProps } from '@wordpress/block-editor';
import ServerSideRender from '@wordpress/server-side-render';
import metadata from './block.json';

registerBlockType( metadata, {
	edit( { attributes } ) {
		return (
			<div { ...useBlockProps() }>
				{ /* Asks the REST API to run the PHP so the editor shows real output. */ }
				<ServerSideRender block={ metadata.name } attributes={ attributes } />
			</div>
		);
	},
	// No save function at all is the same as save: () => null — either way only
	// the self-closing delimiter and the attributes JSON are stored.
} );

render_callback with inner blocks

A dynamic wrapper around static children: the inner blocks save their own markup normally and arrive as $content, while the PHP decides only the outer element. This is the pattern to use when the container is conditional (a link, a role, a permission check) but the contents are ordinary author-written blocks.

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

function sitekit_register_card(): void {
	register_block_type(
		__DIR__ . '/build/card',
		array( 'render_callback' => 'sitekit_render_card' )
	);
}

/**
 * $content holds the inner blocks, already rendered and escaped by core.
 * Wrap it; never escape it again, and never build it by hand.
 */
function sitekit_render_card( array $attributes, string $content, WP_Block $block ): string {
	$href = (string) ( $attributes['href'] ?? '' );

	if ( '' === $href ) {
		return sprintf(
			'<div %s>%s</div>',
			get_block_wrapper_attributes(),
			$content
		);
	}

	return sprintf(
		'<a %s href="%s">%s</a>',
		get_block_wrapper_attributes( array( 'class' => 'is-linked' ) ),
		esc_url( $href ),
		$content
	);
}

Caching an expensive dynamic block

Dynamic means the PHP runs on every uncached page view, including for logged-out visitors on sites with no page cache. If the block calls an HTTP API or runs a heavy query, wrap it in a transient. Leave get_block_wrapper_attributes() inside the cached string only if the supports settings are part of the key — otherwise call it outside the cache.

Example · php
function sitekit_render_feed( array $attributes, string $content, WP_Block $block ): string {
	// The cache key must include every attribute that changes the output,
	// or two blocks with different settings will share one cached result.
	$key  = 'sitekit_feed_' . md5( wp_json_encode( $attributes ) );
	$html = get_transient( $key );

	if ( false !== $html ) {
		return (string) $html;
	}

	$html = sitekit_build_feed_html( $attributes );
	set_transient( $key, $html, 15 * MINUTE_IN_SECONDS );

	return $html;
}

Discussion

  • Be the first to comment on this lesson.