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:
| Field | Type | Attribute name |
|---|---|---|
| Post type | Post Types | postType |
| Number of posts | Number | postsCount |
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_rowssaves 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.
