Dynamic Blocks & ServerSideRender
What are Dynamic Blocks?
Dynamic blocks render their content via PHP at runtime, instead of saving static HTML. They're ideal for:
- Displaying posts or custom queries
- User-specific content
- Real-time data (prices, availability)
- Content that changes based on context
Creating a Dynamic Block
1. Set Up block.json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "theme/latest-posts",
"title": "Latest Posts Grid",
"category": "widgets",
"icon": "grid-view",
"attributes": {
"postsToShow": {
"type": "number",
"default": 6
},
"columns": {
"type": "number",
"default": 3
},
"showExcerpt": {
"type": "boolean",
"default": true
},
"category": {
"type": "number",
"default": 0
}
},
"textdomain": "theme-blocks",
"editorScript": "file:./index.js",
"style": "file:./style-index.css",
"render": "file:./render.php"
}The key difference: "render": "file:./render.php" tells WordPress to use PHP for frontend rendering.
2. Create render.php
<?php
/**
* Renders the Latest Posts block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*/
$posts_to_show = $attributes['postsToShow'] ?? 6;
$columns = $attributes['columns'] ?? 3;
$show_excerpt = $attributes['showExcerpt'] ?? true;
$category = $attributes['category'] ?? 0;
$args = array(
'posts_per_page' => $posts_to_show,
'post_status' => 'publish',
);
if ( $category ) {
$args['cat'] = $category;
}
$query = new WP_Query( $args );
if ( ! $query->have_posts() ) {
return '<p>' . esc_html__( 'No posts found.', 'theme-blocks' ) . '</p>';
}
$wrapper_attributes = get_block_wrapper_attributes( array(
'class' => 'columns-' . $columns,
) );
?>
<div <?php echo $wrapper_attributes; ?>>
<?php while ( $query->have_posts() ) : $query->the_post(); ?>
<article class="post-card">
<?php if ( has_post_thumbnail() ) : ?>
<a href="<?php the_permalink(); ?>" class="post-thumbnail">
<?php the_post_thumbnail( 'medium' ); ?>
</a>
<?php endif; ?>
<div class="post-content">
<h3 class="post-title">
<a href="<?php the_permalink(); ?>">
<?php the_title(); ?>
</a>
</h3>
<?php if ( $show_excerpt ) : ?>
<p class="post-excerpt">
<?php echo wp_trim_words( get_the_excerpt(), 20 ); ?>
</p>
<?php endif; ?>
<time class="post-date">
<?php echo get_the_date(); ?>
</time>
</div>
</article>
<?php endwhile; ?>
<?php wp_reset_postdata(); ?>
</div>3. Save Returns Null
// src/save.js
export default function save() {
return null;
}ServerSideRender in the Editor
Use ServerSideRender to preview PHP output in the editor:
// src/edit.js
import { __ } from '@wordpress/i18n';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl, ToggleControl, SelectControl } from '@wordpress/components';
import ServerSideRender from '@wordpress/server-side-render';
export default function Edit( { attributes, setAttributes } ) {
const { postsToShow, columns, showExcerpt, category } = attributes;
const blockProps = useBlockProps();
return (
<>
<InspectorControls>
<PanelBody title={ __( 'Settings', 'theme-blocks' ) }>
<RangeControl
label={ __( 'Posts to Show', 'theme-blocks' ) }
value={ postsToShow }
onChange={ ( val ) => setAttributes( { postsToShow: val } ) }
min={ 1 }
max={ 12 }
/>
<RangeControl
label={ __( 'Columns', 'theme-blocks' ) }
value={ columns }
onChange={ ( val ) => setAttributes( { columns: val } ) }
min={ 1 }
max={ 4 }
/>
<ToggleControl
label={ __( 'Show Excerpt', 'theme-blocks' ) }
checked={ showExcerpt }
onChange={ ( val ) => setAttributes( { showExcerpt: val } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<ServerSideRender
block="theme/latest-posts"
attributes={ attributes }
/>
</div>
</>
);
}Category Selector
Add a category dropdown using useSelect:
import { useSelect } from '@wordpress/data';
export default function Edit( { attributes, setAttributes } ) {
const { category } = attributes;
// Fetch categories
const categories = useSelect( ( select ) => {
return select( 'core' ).getEntityRecords( 'taxonomy', 'category', {
per_page: -1,
} );
}, [] );
const categoryOptions = [
{ label: __( 'All Categories', 'theme-blocks' ), value: 0 },
...( categories || [] ).map( ( cat ) => ( {
label: cat.name,
value: cat.id,
} ) ),
];
return (
<InspectorControls>
<PanelBody>
<SelectControl
label={ __( 'Category', 'theme-blocks' ) }
value={ category }
options={ categoryOptions }
onChange={ ( val ) => setAttributes( { category: parseInt( val ) } ) }
/>
</PanelBody>
</InspectorControls>
);
}Registering Without @wordpress/create-block
For manual registration in a theme or existing plugin:
// In your plugin or theme functions.php
function theme_register_blocks() {
register_block_type( '/var/www/html' . '/blocks/latest-posts/block.json' );
}
add_action( 'init', 'theme_register_blocks' );Or with inline render callback:
register_block_type( 'theme/latest-posts', array(
'render_callback' => 'theme_render_latest_posts',
'attributes' => array(
'postsToShow' => array(
'type' => 'number',
'default' => 6,
),
),
) );
function theme_render_latest_posts( $attributes, $content, $block ) {
$posts = get_posts( array(
'posts_per_page' => $attributes['postsToShow'],
) );
ob_start();
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
<?php foreach ( $posts as $post ) : ?>
<article>
<h3><?php echo esc_html( $post->post_title ); ?></h3>
</article>
<?php endforeach; ?>
</div>
<?php
return ob_get_clean();
}Block Context
Dynamic blocks can access context from parent blocks:
// In a child block's block.json
{
"usesContext": ["theme/columns"]
}// render.php
$columns = $block->context['theme/columns'] ?? 3;Next Steps
In the next lesson, we'll learn about block variations, transforms, and patterns.