How to Build a Dynamic Gutenberg Block That Lists Posts

A “latest posts” section is one of the most common things on any site: recent articles on the homepage, related tutorials under a post, the newest case studies on a services page. WordPress has a Query Loop block for this, but when you want a fixed design that editors can’t accidentally break, a custom dynamic block is the cleaner solution. This tutorial builds one in PHP.

Dynamic vs static blocks

A static block saves its HTML into the post when you click Update. A dynamic block renders its HTML with PHP every time the page loads. For anything that lists posts you want dynamic: when you publish a new post, every “latest posts” block on the site updates by itself.

When the Query Loop block is enough

Core’s Query Loop block can already list posts with a design built from blocks. Use it when editors should be able to change the layout. Build your own block when:

  • the card design must stay exactly as designed,
  • you need logic the Query Loop can’t express (custom meta queries, mixing post types, special ordering),
  • you want one simple block with two or three settings instead of a nested block structure.

Step 1: the settings editors can change

In FanCoolo WP, create a block called “Latest posts” and add two attributes in the Attributes tab:

FieldTypeAttribute name
Post typePost TypespostType
Number of postsNumberpostsCount

They appear in the Gutenberg sidebar as native controls.

Step 2: query and render

In the Content tab:

<?php
$post_type = $attributes['postType'] ?? 'post';
$count     = max( 1, (int) ( $attributes['postsCount'] ?? 3 ) );

$query = new WP_Query( [
    'post_type'           => $post_type,
    'posts_per_page'      => $count,
    'post_status'         => 'publish',
    'ignore_sticky_posts' => true,
    'no_found_rows'       => true, // we don't paginate, so skip the COUNT query
] );

if ( ! $query->have_posts() ) {
    return;
}
?>
<div <?php echo get_block_wrapper_attributes( [ 'class' => 'latest-posts' ] ); ?>>
  <?php while ( $query->have_posts() ) : $query->the_post(); ?>
    <a class="latest-posts__card" href="<?php the_permalink(); ?>">
      <?php if ( has_post_thumbnail() ) the_post_thumbnail( 'medium_large', [ 'loading' => 'lazy' ] ); ?>
      <h3><?php the_title(); ?></h3>
      <p><?php echo esc_html( wp_trim_words( get_the_excerpt(), 20 ) ); ?></p>
    </a>
  <?php endwhile; ?>
</div>
<?php wp_reset_postdata(); ?>

A few details that matter:

  • wp_reset_postdata() restores the main query, so the rest of the page (comments, sidebars) still knows which post it’s on.
  • no_found_rows saves a database query when you don’t need pagination.
  • Lazy-loaded images keep the page fast when the block is below the fold.

Step 3: style the cards

In the Style tab, a simple responsive grid:

.latest-posts {
  display: grid;
  gap: 2rem;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));

  &__card {
    display: flex;
    flex-direction: column;
    text-decoration: none;
    color: inherit;

    img { border-radius: 12px; aspect-ratio: 16 / 9; object-fit: cover; }
  }
}

The same SCSS is compiled for the editor and the front end, so the preview in Gutenberg matches the live page.

Step 4: a nicer editor preview

FanCoolo passes helper variables to every template. $isEditor is true while the block is shown in the editor, which is handy for placeholders:

<?php if ( $isEditor && ! $query->have_posts() ) : ?>
  <p>No posts yet. Publish a post to see it here.</p>
<?php endif; ?>

Ideas to extend it

  • Add a Taxonomies attribute to filter by category.
  • Add a Toggle to show or hide the excerpt.
  • Move the card markup into a Symbol (a reusable PHP component) and reuse it in a “Related posts” block.
Dynamic blocks without React

Dynamic blocks without React

FanCoolo WP renders blocks in PHP, so WP_Query, post meta and ACF fields work out of the box, with native sidebar controls.