How to Build a Dynamic Gutenberg Block in WordPress (with Code Examples)

dynamic blocks

Gutenberg blocks are awesome—but sometimes you don’t want to hardcode static content inside save.js. You want the block to render dynamically, pulling in live data from the database, custom post types, or even external APIs.

In this article, I’ll show you how to create a dynamic Gutenberg block using render_callback—with real code you can adapt for your own projects.

🧱 What Is a Dynamic Block?

A dynamic block is one that’s rendered by PHP on the frontend instead of being saved as static HTML in the post content. You define a render_callback, and WordPress will run it each time the block is shown.

Perfect for:

  • Post lists
  • Custom queries
  • Real-time values (dates, stats, user info)
  • API-connected content

📁 Folder Structure

Here’s how I typically organize a dynamic block inside my plugin:

my-plugin/
├── blocks/
│   └── latest-posts/
│       ├── block.json
│       ├── edit.js
│       ├── index.js
│       ├── style.scss
│       └── render.php

📝 Step 1: block.json

{
  "apiVersion": 2,
  "name": "myplugin/latest-posts",
  "title": "Latest Posts",
  "category": "widgets",
  "icon": "megaphone",
  "supports": { "html": false },
  "attributes": {
    "postsToShow": {
      "type": "number",
      "default": 3
    }
  },
  "editorScript": "file:./index.js",
  "style": "file:./style.css",
  "render": "file:./render.php"
}

🧠 Step 2: edit.js

import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, RangeControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

export default function Edit({ attributes, setAttributes }) {
  return (
    <>
      <InspectorControls>
        <PanelBody title={__('Block Settings')}>
          <RangeControl
            label={__('Number of posts')}
            value={attributes.postsToShow}
            onChange={(value) => setAttributes({ postsToShow: value })}
            min={1}
            max={10}
          />
        </PanelBody>
      </InspectorControls>
      <p>{__('This block shows the latest posts on the frontend.')}</p>
    </>
  );
}

🧩 Step 3: render.php

<?php
$atts = $attributes ?? [];
$posts_to_show = isset($atts['postsToShow']) ? intval($atts['postsToShow']) : 3;

$args = [
  'post_type'      => 'post',
  'posts_per_page' => $posts_to_show,
];

$posts = get_posts($args);

if (!$posts) {
  return '<p>No posts found.</p>';
}

$output = '<ul class="latest-posts">';
foreach ($posts as $post) {
  $output .= sprintf(
    '<li><a href="%s">%s</a></li>',
    esc_url(get_permalink($post)),
    esc_html(get_the_title($post))
  );
}
$output .= '</ul>';

echo $output;

🧱 Step 4: Register the Block

function myplugin_register_blocks() {
  register_block_type(__DIR__ . '/blocks/latest-posts');
}
add_action('init', 'myplugin_register_blocks');

🧪 Bonus Ideas

  • Add a taxonomy filter (category dropdown)
  • Make it support custom post types
  • Return results as cards with Flexbox or CSS Grid
  • Output structured HTML + schema for SEO

✅ Final Thoughts

Once you’ve built one dynamic block, the possibilities open up. You can turn Gutenberg into a full content delivery system—with live queries, third-party data, and reusable frontend logic.

And the best part? It’s all native WordPress. No shortcode spaghetti. No plugin bloat.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *