LESSON 5 โฑ๏ธ 15 min read

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>
Always use get_block_wrapper_attributes(): This ensures proper block classes and alignment support work correctly.

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.

๐ŸŽฏ Lesson Complete! You can now create dynamic blocks with PHP rendering and live editor previews.