By Devport Team | Last updated: 2025-07-12 | 17 min read

How to Create Dynamic Gutenberg Blocks with PHP and the REST API

While static blocks are perfect for content that doesn't change, dynamic blocks unlock the true power of Gutenberg by rendering content on the server side and fetching real-time data. This approach is essential for displaying posts, user data, external API content, or any information that needs to update automatically.

In this comprehensive guide, we'll explore how to create dynamic Gutenberg blocks using PHP for server-side rendering and the WordPress REST API for real-time data fetching. You'll learn when to use dynamic blocks, how to implement them efficiently, and strategies for optimal performance.

Table of Contents

  1. Static vs Dynamic Blocks
  2. Server-Side Rendering Setup
  3. REST API Integration
  4. Real-Time Data Fetching
  5. Caching Strategies
  6. Complex Block Examples

Static vs Dynamic Blocks

Understanding the Difference

Static Blocks

Dynamic Blocks

When to Use Dynamic Blocks

Use dynamic blocks when: - ✅ Displaying posts, pages, or custom post types - ✅ Showing user-specific content - ✅ Integrating external API data - ✅ Content that updates frequently - ✅ Complex queries or calculations - ✅ Conditional content based on context

Architecture Comparison

// Static Block - Content saved to database
save: ({ attributes }) => {
    return <div>{ attributes.content }</div>;
}

// Dynamic Block - No save function
save: () => {
    return null; // PHP handles rendering
}

Server-Side Rendering Setup

Basic Dynamic Block Structure

1. Plugin Setup

File: dynamic-block-plugin.php

<?php
/**
 * Plugin Name: Dynamic Block Examples
 * Description: Examples of dynamic Gutenberg blocks
 * Version: 1.0.0
 * Author: Your Name
 */

// Exit if accessed directly
if (!defined('ABSPATH')) {
    exit;
}

// Register block assets
function dynamic_blocks_init() {
    // Register block editor script
    wp_register_script(
        'dynamic-blocks-editor',
        plugins_url('build/index.js', __FILE__),
        array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-data'),
        filemtime(plugin_dir_path(__FILE__) . 'build/index.js')
    );

    // Register block editor styles
    wp_register_style(
        'dynamic-blocks-editor',
        plugins_url('build/editor.css', __FILE__),
        array('wp-edit-blocks'),
        filemtime(plugin_dir_path(__FILE__) . 'build/editor.css')
    );

    // Register frontend styles
    wp_register_style(
        'dynamic-blocks-frontend',
        plugins_url('build/style.css', __FILE__),
        array(),
        filemtime(plugin_dir_path(__FILE__) . 'build/style.css')
    );

    // Register dynamic blocks
    register_block_type('dynamic-blocks/latest-posts', array(
        'editor_script' => 'dynamic-blocks-editor',
        'editor_style'  => 'dynamic-blocks-editor',
        'style'         => 'dynamic-blocks-frontend',
        'render_callback' => 'render_latest_posts_block',
        'attributes' => array(
            'numberOfPosts' => array(
                'type' => 'number',
                'default' => 3
            ),
            'displayFeaturedImage' => array(
                'type' => 'boolean',
                'default' => true
            ),
            'order' => array(
                'type' => 'string',
                'default' => 'desc'
            ),
            'orderBy' => array(
                'type' => 'string',
                'default' => 'date'
            ),
            'categories' => array(
                'type' => 'array',
                'default' => array()
            ),
            'selectedAuthor' => array(
                'type' => 'number',
                'default' => 0
            ),
            'displayExcerpt' => array(
                'type' => 'boolean',
                'default' => true
            ),
            'excerptLength' => array(
                'type' => 'number',
                'default' => 20
            ),
            'displayDate' => array(
                'type' => 'boolean',
                'default' => true
            ),
            'displayAuthor' => array(
                'type' => 'boolean',
                'default' => true
            ),
            'columnsDesktop' => array(
                'type' => 'number',
                'default' => 3
            ),
            'columnsTablet' => array(
                'type' => 'number',
                'default' => 2
            ),
            'columnsMobile' => array(
                'type' => 'number',
                'default' => 1
            )
        )
    ));
}
add_action('init', 'dynamic_blocks_init');

2. Render Callback Function

/**
 * Render callback for latest posts block
 */
function render_latest_posts_block($attributes, $content) {
    // Extract attributes with defaults
    $args = array(
        'posts_per_page' => $attributes['numberOfPosts'],
        'order' => $attributes['order'],
        'orderby' => $attributes['orderBy'],
        'post_status' => 'publish',
        'ignore_sticky_posts' => 1,
    );

    // Add category filter if specified
    if (!empty($attributes['categories'])) {
        $args['category__in'] = $attributes['categories'];
    }

    // Add author filter if specified
    if ($attributes['selectedAuthor'] > 0) {
        $args['author'] = $attributes['selectedAuthor'];
    }

    // Get posts
    $recent_posts = new WP_Query($args);

    // Start output buffering
    ob_start();

    // Generate unique ID for this block instance
    $block_id = 'dynamic-posts-' . uniqid();

    // Add responsive CSS
    ?>
    <style>
        #<?php echo esc_attr($block_id); ?> {
            --columns-desktop: <?php echo esc_attr($attributes['columnsDesktop']); ?>;
            --columns-tablet: <?php echo esc_attr($attributes['columnsTablet']); ?>;
            --columns-mobile: <?php echo esc_attr($attributes['columnsMobile']); ?>;
        }
    </style>
    <?php

    if ($recent_posts->have_posts()) : ?>
        <div id="<?php echo esc_attr($block_id); ?>" class="wp-block-dynamic-blocks-latest-posts">
            <div class="posts-grid">
                <?php while ($recent_posts->have_posts()) : $recent_posts->the_post(); ?>
                    <article class="post-item">
                        <?php if ($attributes['displayFeaturedImage'] && has_post_thumbnail()) : ?>
                            <div class="post-thumbnail">
                                <a href="<?php the_permalink(); ?>">
                                    <?php the_post_thumbnail('medium'); ?>
                                </a>
                            </div>
                        <?php endif; ?>

                        <div class="post-content">
                            <h3 class="post-title">
                                <a href="<?php the_permalink(); ?>">
                                    <?php the_title(); ?>
                                </a>
                            </h3>

                            <?php if ($attributes['displayDate'] || $attributes['displayAuthor']) : ?>
                                <div class="post-meta">
                                    <?php if ($attributes['displayDate']) : ?>
                                        <time class="post-date" datetime="<?php echo get_the_date('c'); ?>">
                                            <?php echo get_the_date(); ?>
                                        </time>
                                    <?php endif; ?>

                                    <?php if ($attributes['displayAuthor']) : ?>
                                        <span class="post-author">
                                            <?php _e('by', 'dynamic-blocks'); ?>
                                            <a href="<?php echo get_author_posts_url(get_the_author_meta('ID')); ?>">
                                                <?php the_author(); ?>
                                            </a>
                                        </span>
                                    <?php endif; ?>
                                </div>
                            <?php endif; ?>

                            <?php if ($attributes['displayExcerpt']) : ?>
                                <div class="post-excerpt">
                                    <?php echo wp_trim_words(get_the_excerpt(), $attributes['excerptLength']); ?>
                                </div>
                            <?php endif; ?>
                        </div>
                    </article>
                <?php endwhile; ?>
            </div>
        </div>
    <?php else : ?>
        <p class="no-posts-found"><?php _e('No posts found.', 'dynamic-blocks'); ?></p>
    <?php endif;

    // Reset post data
    wp_reset_postdata();

    // Return buffered content
    return ob_get_clean();
}

JavaScript Block Registration

File: src/latest-posts/index.js

import { registerBlockType } from '@wordpress/blocks';
import { __ } from '@wordpress/i18n';
import Edit from './edit';
import icon from './icon';

registerBlockType('dynamic-blocks/latest-posts', {
    title: __('Dynamic Latest Posts', 'dynamic-blocks'),
    description: __('Display latest posts with advanced filtering', 'dynamic-blocks'),
    category: 'widgets',
    icon,
    keywords: [
        __('posts', 'dynamic-blocks'),
        __('recent', 'dynamic-blocks'),
        __('dynamic', 'dynamic-blocks')
    ],
    supports: {
        align: ['wide', 'full'],
        html: false,
    },
    edit: Edit,
    save: () => null, // Dynamic block - no save function
});

REST API Integration

Creating Custom REST Endpoints

/**
 * Register custom REST API endpoints
 */
function register_dynamic_blocks_rest_routes() {
    // Custom endpoint for filtered posts
    register_rest_route('dynamic-blocks/v1', '/posts', array(
        'methods' => 'GET',
        'callback' => 'get_filtered_posts',
        'permission_callback' => '__return_true',
        'args' => array(
            'per_page' => array(
                'type' => 'integer',
                'default' => 10,
                'minimum' => 1,
                'maximum' => 100,
            ),
            'categories' => array(
                'type' => 'array',
                'items' => array(
                    'type' => 'integer'
                ),
            ),
            'search' => array(
                'type' => 'string',
                'sanitize_callback' => 'sanitize_text_field',
            ),
            'author' => array(
                'type' => 'integer',
            ),
            'exclude' => array(
                'type' => 'array',
                'items' => array(
                    'type' => 'integer'
                ),
            ),
        ),
    ));

    // Endpoint for post statistics
    register_rest_route('dynamic-blocks/v1', '/stats', array(
        'methods' => 'GET',
        'callback' => 'get_post_statistics',
        'permission_callback' => function() {
            return current_user_can('edit_posts');
        },
    ));
}
add_action('rest_api_init', 'register_dynamic_blocks_rest_routes');

/**
 * REST callback for filtered posts
 */
function get_filtered_posts($request) {
    $args = array(
        'posts_per_page' => $request->get_param('per_page'),
        'post_status' => 'publish',
    );

    if ($categories = $request->get_param('categories')) {
        $args['category__in'] = $categories;
    }

    if ($search = $request->get_param('search')) {
        $args['s'] = $search;
    }

    if ($author = $request->get_param('author')) {
        $args['author'] = $author;
    }

    if ($exclude = $request->get_param('exclude')) {
        $args['post__not_in'] = $exclude;
    }

    $posts = get_posts($args);
    $formatted_posts = array();

    foreach ($posts as $post) {
        $formatted_posts[] = array(
            'id' => $post->ID,
            'title' => get_the_title($post),
            'excerpt' => get_the_excerpt($post),
            'link' => get_permalink($post),
            'date' => get_the_date('c', $post),
            'author' => array(
                'id' => $post->post_author,
                'name' => get_the_author_meta('display_name', $post->post_author),
                'link' => get_author_posts_url($post->post_author),
            ),
            'featured_image' => get_the_post_thumbnail_url($post, 'medium'),
            'categories' => wp_get_post_categories($post->ID, array('fields' => 'all')),
        );
    }

    return rest_ensure_response($formatted_posts);
}

Consuming REST API in Block Editor

File: src/latest-posts/edit.js

import { __ } from '@wordpress/i18n';
import { 
    useBlockProps, 
    InspectorControls,
    BlockControls,
    AlignmentToolbar
} from '@wordpress/block-editor';
import {
    PanelBody,
    RangeControl,
    ToggleControl,
    SelectControl,
    Spinner,
    Placeholder,
    ToolbarGroup,
    ToolbarButton,
} from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { useState, useEffect } from '@wordpress/element';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';

export default function Edit({ attributes, setAttributes }) {
    const {
        numberOfPosts,
        displayFeaturedImage,
        displayExcerpt,
        displayDate,
        displayAuthor,
        categories,
        selectedAuthor,
        order,
        orderBy,
        columnsDesktop,
        columnsTablet,
        columnsMobile,
    } = attributes;

    const [posts, setPosts] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    // Fetch categories for the filter
    const allCategories = useSelect((select) => {
        return select('core').getEntityRecords('taxonomy', 'category', {
            per_page: -1,
        });
    }, []);

    // Fetch authors for the filter
    const authors = useSelect((select) => {
        return select('core').getUsers({ who: 'authors' });
    }, []);

    // Fetch posts using custom endpoint
    useEffect(() => {
        setIsLoading(true);

        const path = addQueryArgs('/dynamic-blocks/v1/posts', {
            per_page: numberOfPosts,
            categories: categories.length ? categories : undefined,
            author: selectedAuthor || undefined,
        });

        apiFetch({ path })
            .then((fetchedPosts) => {
                setPosts(fetchedPosts);
                setIsLoading(false);
            })
            .catch(() => {
                setPosts([]);
                setIsLoading(false);
            });
    }, [numberOfPosts, categories, selectedAuthor]);

    const blockProps = useBlockProps({
        className: 'dynamic-latest-posts-editor',
    });

    // Loading state
    if (isLoading) {
        return (
            <div {...blockProps}>
                <Placeholder
                    icon="admin-post"
                    label={__('Dynamic Latest Posts', 'dynamic-blocks')}
                >
                    <Spinner />
                </Placeholder>
            </div>
        );
    }

    // No posts found
    if (!posts.length) {
        return (
            <div {...blockProps}>
                <Placeholder
                    icon="admin-post"
                    label={__('Dynamic Latest Posts', 'dynamic-blocks')}
                >
                    {__('No posts found.', 'dynamic-blocks')}
                </Placeholder>
            </div>
        );
    }

    return (
        <>
            <InspectorControls>
                <PanelBody title={__('Post Settings', 'dynamic-blocks')}>
                    <RangeControl
                        label={__('Number of Posts', 'dynamic-blocks')}
                        value={numberOfPosts}
                        onChange={(value) => setAttributes({ numberOfPosts: value })}
                        min={1}
                        max={20}
                    />

                    {allCategories && (
                        <SelectControl
                            multiple
                            label={__('Categories', 'dynamic-blocks')}
                            value={categories}
                            options={allCategories.map((cat) => ({
                                label: cat.name,
                                value: cat.id,
                            }))}
                            onChange={(value) => setAttributes({ 
                                categories: value.map(v => parseInt(v)) 
                            })}
                        />
                    )}

                    {authors && (
                        <SelectControl
                            label={__('Author', 'dynamic-blocks')}
                            value={selectedAuthor}
                            options={[
                                { label: __('All Authors', 'dynamic-blocks'), value: 0 },
                                ...authors.map((author) => ({
                                    label: author.name,
                                    value: author.id,
                                }))
                            ]}
                            onChange={(value) => setAttributes({ 
                                selectedAuthor: parseInt(value) 
                            })}
                        />
                    )}

                    <SelectControl
                        label={__('Order By', 'dynamic-blocks')}
                        value={orderBy}
                        options={[
                            { label: __('Date', 'dynamic-blocks'), value: 'date' },
                            { label: __('Title', 'dynamic-blocks'), value: 'title' },
                            { label: __('Random', 'dynamic-blocks'), value: 'rand' },
                        ]}
                        onChange={(value) => setAttributes({ orderBy: value })}
                    />

                    <SelectControl
                        label={__('Order', 'dynamic-blocks')}
                        value={order}
                        options={[
                            { label: __('Descending', 'dynamic-blocks'), value: 'desc' },
                            { label: __('Ascending', 'dynamic-blocks'), value: 'asc' },
                        ]}
                        onChange={(value) => setAttributes({ order: value })}
                    />
                </PanelBody>

                <PanelBody title={__('Display Settings', 'dynamic-blocks')}>
                    <ToggleControl
                        label={__('Display Featured Image', 'dynamic-blocks')}
                        checked={displayFeaturedImage}
                        onChange={(value) => setAttributes({ displayFeaturedImage: value })}
                    />

                    <ToggleControl
                        label={__('Display Excerpt', 'dynamic-blocks')}
                        checked={displayExcerpt}
                        onChange={(value) => setAttributes({ displayExcerpt: value })}
                    />

                    <ToggleControl
                        label={__('Display Date', 'dynamic-blocks')}
                        checked={displayDate}
                        onChange={(value) => setAttributes({ displayDate: value })}
                    />

                    <ToggleControl
                        label={__('Display Author', 'dynamic-blocks')}
                        checked={displayAuthor}
                        onChange={(value) => setAttributes({ displayAuthor: value })}
                    />
                </PanelBody>

                <PanelBody title={__('Layout Settings', 'dynamic-blocks')}>
                    <RangeControl
                        label={__('Desktop Columns', 'dynamic-blocks')}
                        value={columnsDesktop}
                        onChange={(value) => setAttributes({ columnsDesktop: value })}
                        min={1}
                        max={6}
                    />

                    <RangeControl
                        label={__('Tablet Columns', 'dynamic-blocks')}
                        value={columnsTablet}
                        onChange={(value) => setAttributes({ columnsTablet: value })}
                        min={1}
                        max={4}
                    />

                    <RangeControl
                        label={__('Mobile Columns', 'dynamic-blocks')}
                        value={columnsMobile}
                        onChange={(value) => setAttributes({ columnsMobile: value })}
                        min={1}
                        max={2}
                    />
                </PanelBody>
            </InspectorControls>

            <div {...blockProps}>
                <div className="posts-grid" style={{
                    gridTemplateColumns: `repeat(${columnsDesktop}, 1fr)`
                }}>
                    {posts.map((post) => (
                        <article key={post.id} className="post-item">
                            {displayFeaturedImage && post.featured_image && (
                                <div className="post-thumbnail">
                                    <img src={post.featured_image} alt="" />
                                </div>
                            )}
                            <div className="post-content">
                                <h3 className="post-title">{post.title}</h3>

                                {(displayDate || displayAuthor) && (
                                    <div className="post-meta">
                                        {displayDate && (
                                            <time className="post-date">
                                                {new Date(post.date).toLocaleDateString()}
                                            </time>
                                        )}
                                        {displayAuthor && (
                                            <span className="post-author">
                                                by {post.author.name}
                                            </span>
                                        )}
                                    </div>
                                )}

                                {displayExcerpt && (
                                    <div 
                                        className="post-excerpt"
                                        dangerouslySetInnerHTML={{ __html: post.excerpt }}
                                    />
                                )}
                            </div>
                        </article>
                    ))}
                </div>
            </div>
        </>
    );
}

Real-Time Data Fetching

Live Search Implementation

// Add to edit.js
import { useDebouncedInput } from '@wordpress/compose';

function LiveSearchPosts({ attributes, setAttributes }) {
    const [searchTerm, setSearchTerm] = useState('');
    const [searchResults, setSearchResults] = useState([]);
    const [isSearching, setIsSearching] = useState(false);

    // Debounced search function
    const debouncedSearch = useDebouncedInput((value) => {
        if (!value) {
            setSearchResults([]);
            return;
        }

        setIsSearching(true);

        apiFetch({
            path: addQueryArgs('/wp/v2/search', {
                search: value,
                type: 'post',
                subtype: 'post',
                per_page: 5,
            }),
        })
        .then((results) => {
            setSearchResults(results);
            setIsSearching(false);
        })
        .catch(() => {
            setSearchResults([]);
            setIsSearching(false);
        });
    }, 500);

    return (
        <div className="live-search-container">
            <TextControl
                label={__('Search Posts', 'dynamic-blocks')}
                value={searchTerm}
                onChange={(value) => {
                    setSearchTerm(value);
                    debouncedSearch(value);
                }}
                placeholder={__('Start typing to search...', 'dynamic-blocks')}
            />

            {isSearching && <Spinner />}

            {searchResults.length > 0 && (
                <ul className="search-results">
                    {searchResults.map((result) => (
                        <li key={result.id}>
                            <Button
                                isLink
                                onClick={() => {
                                    // Add post to selected posts
                                    const currentPosts = attributes.selectedPosts || [];
                                    if (!currentPosts.includes(result.id)) {
                                        setAttributes({
                                            selectedPosts: [...currentPosts, result.id]
                                        });
                                    }
                                }}
                            >
                                {result.title}
                            </Button>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
}

WebSocket Integration for Real-Time Updates

// Add real-time capabilities with WordPress Heartbeat API
function dynamic_blocks_heartbeat_received($response, $data) {
    if (empty($data['dynamic_blocks_check_updates'])) {
        return $response;
    }

    // Check for new posts since last check
    $last_check = $data['dynamic_blocks_check_updates']['last_check'];

    $new_posts = get_posts(array(
        'date_query' => array(
            array(
                'after' => $last_check,
            ),
        ),
        'posts_per_page' => 5,
        'fields' => 'ids',
    ));

    if (!empty($new_posts)) {
        $response['dynamic_blocks_new_posts'] = $new_posts;
    }

    return $response;
}
add_filter('heartbeat_received', 'dynamic_blocks_heartbeat_received', 10, 2);

Caching Strategies

Transient Caching for Performance

/**
 * Render callback with caching
 */
function render_cached_posts_block($attributes) {
    // Generate cache key based on attributes
    $cache_key = 'dynamic_posts_' . md5(serialize($attributes));

    // Try to get cached content
    $cached_content = get_transient($cache_key);

    if ($cached_content !== false) {
        return $cached_content;
    }

    // Generate content if not cached
    $content = generate_posts_content($attributes);

    // Cache for 1 hour
    set_transient($cache_key, $content, HOUR_IN_SECONDS);

    return $content;
}

/**
 * Clear cache when posts are updated
 */
function clear_dynamic_blocks_cache($post_id) {
    // Clear all dynamic posts transients
    global $wpdb;
    $wpdb->query(
        "DELETE FROM {$wpdb->options} 
         WHERE option_name LIKE '_transient_dynamic_posts_%'"
    );
}
add_action('save_post', 'clear_dynamic_blocks_cache');
add_action('deleted_post', 'clear_dynamic_blocks_cache');
add_action('wp_trash_post', 'clear_dynamic_blocks_cache');

Client-Side Caching with Service Workers

// Service worker for caching API responses
self.addEventListener('fetch', (event) => {
    if (event.request.url.includes('/wp-json/dynamic-blocks/')) {
        event.respondWith(
            caches.open('dynamic-blocks-v1').then((cache) => {
                return cache.match(event.request).then((response) => {
                    return response || fetch(event.request).then((response) => {
                        // Cache successful responses
                        if (response.status === 200) {
                            cache.put(event.request, response.clone());
                        }
                        return response;
                    });
                });
            })
        );
    }
});

Complex Block Examples

Advanced Product Grid with Filters

/**
 * Complex product grid block with AJAX filtering
 */
function render_product_grid_block($attributes) {
    ob_start();
    ?>
    <div class="product-grid-block" data-attributes='<?php echo json_encode($attributes); ?>'>
        <div class="product-filters">
            <select class="filter-category">
                <option value=""><?php _e('All Categories', 'dynamic-blocks'); ?></option>
                <?php
                $categories = get_terms('product_cat');
                foreach ($categories as $category) {
                    echo '<option value="' . $category->term_id . '">' . $category->name . '</option>';
                }
                ?>
            </select>

            <select class="filter-price">
                <option value=""><?php _e('All Prices', 'dynamic-blocks'); ?></option>
                <option value="0-50">$0 - $50</option>
                <option value="50-100">$50 - $100</option>
                <option value="100+"><?php _e('$100+', 'dynamic-blocks'); ?></option>
            </select>

            <input type="text" class="filter-search" placeholder="<?php _e('Search products...', 'dynamic-blocks'); ?>">
        </div>

        <div class="products-container">
            <?php echo render_products($attributes); ?>
        </div>

        <div class="loading-spinner" style="display: none;">
            <span class="spinner"></span>
        </div>
    </div>

    <script>
    (function($) {
        $('.product-grid-block').each(function() {
            const $block = $(this);
            const attributes = $block.data('attributes');
            let filterTimeout;

            function updateProducts() {
                clearTimeout(filterTimeout);
                filterTimeout = setTimeout(() => {
                    const filters = {
                        category: $block.find('.filter-category').val(),
                        price_range: $block.find('.filter-price').val(),
                        search: $block.find('.filter-search').val(),
                        ...attributes
                    };

                    $block.find('.loading-spinner').show();

                    $.ajax({
                        url: '<?php echo admin_url('admin-ajax.php'); ?>',
                        type: 'POST',
                        data: {
                            action: 'filter_products',
                            filters: filters,
                            nonce: '<?php echo wp_create_nonce('filter_products'); ?>'
                        },
                        success: function(response) {
                            $block.find('.products-container').html(response);
                            $block.find('.loading-spinner').hide();
                        }
                    });
                }, 300);
            }

            $block.find('.filter-category, .filter-price').on('change', updateProducts);
            $block.find('.filter-search').on('input', updateProducts);
        });
    })(jQuery);
    </script>
    <?php
    return ob_get_clean();
}

// AJAX handler for product filtering
add_action('wp_ajax_filter_products', 'handle_product_filter');
add_action('wp_ajax_nopriv_filter_products', 'handle_product_filter');

function handle_product_filter() {
    check_ajax_referer('filter_products', 'nonce');

    $filters = $_POST['filters'];
    echo render_products($filters);

    wp_die();
}

Dynamic Data Dashboard Block

// Complex dashboard block with multiple data sources
const DashboardBlock = ({ attributes, setAttributes }) => {
    const [stats, setStats] = useState({
        posts: 0,
        comments: 0,
        users: 0,
        views: 0
    });
    const [chartData, setChartData] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        // Fetch multiple data sources in parallel
        Promise.all([
            apiFetch({ path: '/dynamic-blocks/v1/stats' }),
            apiFetch({ path: '/dynamic-blocks/v1/chart-data' }),
            apiFetch({ path: '/wp/v2/posts?per_page=1' }),
            apiFetch({ path: '/wp/v2/comments?per_page=1' }),
            apiFetch({ path: '/wp/v2/users?per_page=1' })
        ]).then(([statsData, chartData, posts, comments, users]) => {
            setStats({
                ...statsData,
                totalPosts: posts.headers['X-WP-Total'],
                totalComments: comments.headers['X-WP-Total'],
                totalUsers: users.headers['X-WP-Total']
            });
            setChartData(chartData);
            setIsLoading(false);
        });
    }, []);

    return (
        <div className="dashboard-block">
            {isLoading ? (
                <Spinner />
            ) : (
                <>
                    <div className="stats-grid">
                        <StatCard 
                            title="Total Posts" 
                            value={stats.totalPosts} 
                            icon="admin-post"
                        />
                        <StatCard 
                            title="Comments" 
                            value={stats.totalComments} 
                            icon="admin-comments"
                        />
                        <StatCard 
                            title="Users" 
                            value={stats.totalUsers} 
                            icon="admin-users"
                        />
                        <StatCard 
                            title="Page Views" 
                            value={stats.views} 
                            icon="visibility"
                        />
                    </div>

                    <div className="chart-container">
                        <LineChart data={chartData} />
                    </div>

                    <div className="recent-activity">
                        <RecentPosts limit={5} />
                        <RecentComments limit={5} />
                    </div>
                </>
            )}
        </div>
    );
};

Best Practices and Performance Tips

1. Efficient Queries

// Use specific fields to reduce memory usage
$args = array(
    'fields' => 'ids', // Only get post IDs
    'no_found_rows' => true, // Skip pagination calculations
    'update_post_meta_cache' => false, // Skip meta cache
    'update_post_term_cache' => false, // Skip term cache
);

2. Proper Error Handling

function safe_render_block($attributes) {
    try {
        return render_block_content($attributes);
    } catch (Exception $e) {
        if (WP_DEBUG) {
            return '<div class="block-error">' . esc_html($e->getMessage()) . '</div>';
        }
        return '<div class="block-error">' . __('Block temporarily unavailable.', 'dynamic-blocks') . '</div>';
    }
}

3. Security Best Practices

// Always validate and sanitize
$post_id = absint($attributes['postId']);
$title = sanitize_text_field($attributes['title']);
$content = wp_kses_post($attributes['content']);

// Check capabilities
if (!current_user_can('edit_posts')) {
    return '';
}

// Use nonces for AJAX
wp_localize_script('dynamic-blocks', 'dynamicBlocksAjax', array(
    'ajaxurl' => admin_url('admin-ajax.php'),
    'nonce' => wp_create_nonce('dynamic_blocks_nonce')
));

Conclusion

Dynamic Gutenberg blocks unlock powerful possibilities for creating interactive, data-driven content in WordPress. By combining PHP server-side rendering with REST API integration, you can build blocks that display real-time information while maintaining optimal performance.

Key takeaways: - Use dynamic blocks for frequently changing content - Leverage WordPress REST API for real-time updates - Implement proper caching strategies - Always consider security and performance - Test thoroughly with different data scenarios

The examples in this guide provide a foundation for building sophisticated dynamic blocks. Start with simple implementations and gradually add complexity as you become comfortable with the patterns and best practices.


Ready to explore more advanced WordPress development? Check out our Ultimate Guide to Gutenberg and Full-Site Editing for comprehensive tutorials and techniques.