Dynamic blocks in WordPress render their output via a PHP callback at request time, making them ideal for content that changes frequently — recent posts, live counts, or user-specific data. Unlike static blocks, the block content is never stored in post_content.
Problem: A Gutenberg block needs to render fresh server-side data — post counts, dynamic listings, live widget output — but the static save function stores HTML at save time and becomes stale immediately.
Solution: Register a dynamic block by omitting the save function and providing a PHP render_callback in register_block_type(). The callback receives the block's attributes and inner content, executes fresh PHP on every page request, and uses get_block_wrapper_attributes() to output the correct wrapper with editor-generated classes.
The examples below register a dynamic block with register_block_type, write a PHP render callback that queries the database, and show the corresponding block.json with attributes and supports.
// register-block.php — called from your plugin's main file
function myplugin_register_latest_posts_block(): void {
register_block_type(
__DIR__ . '/blocks/latest-posts', // path to folder containing block.json
[
'render_callback' => 'myplugin_render_latest_posts',
]
);
}
add_action( 'init', 'myplugin_register_latest_posts_block' );
function myplugin_render_latest_posts( array $attributes, string $content, WP_Block $block ): string {
$count = absint( $attributes['postsCount'] ?? 5 );
$category = absint( $attributes['categoryId'] ?? 0 );
$query_args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => $count,
'cat' => $category ?: null,
'no_found_rows' => true,
];
$posts = get_posts( $query_args );
if ( empty( $posts ) ) return '';
$wrapper_attrs = get_block_wrapper_attributes( [ 'class' => 'latest-posts-block' ] );
$html = "";
foreach ( $posts as $post ) {
$html .= sprintf(
'- %s
',
esc_url( get_permalink( $post ) ),
esc_html( get_the_title( $post ) )
);
}
$html .= '
';
return $html;
}
The matching block.json with attributes:
{
"apiVersion": 3,
"name": "myplugin/latest-posts",
"title": "Latest Posts",
"category": "widgets",
"icon": "list-view",
"description": "Displays the most recent posts, optionally filtered by category.",
"attributes": {
"postsCount": {
"type": "integer",
"default": 5,
"minimum": 1,
"maximum": 20
},
"categoryId": {
"type": "integer",
"default": 0
}
},
"supports": {
"html": false,
"align": ["wide", "full"],
"color": { "background": true, "text": true },
"spacing": { "padding": true, "margin": true }
},
"editorScript": "file:./index.js",
"style": "file:./style.css"
}
NOTE: Use get_block_wrapper_attributes() in the render callback to automatically output the class, style, and id attributes that the block editor adds — this ensures color/spacing supports work correctly on the front end.