If you’re following this blog to learn a block development, you know we’ve covered so far
- How to Build Repeater Block in Gutenberg
- How to Use Inner Blocks in Gutenberg
- How to Build Carousel Block for Gutenberg
- Upload Multiple Images Using MediaUpload Component
- Use of RichText and MediaUpload Component
If this is the first time you’re visiting this blog, I’d suggest reading the above articles to become familiar with block development.
In this tutorial, we’ll study how to build a post grid using a custom block. On your WordPress websites, it’s common to display a post grid. Here I used the word post but it can be a page or custom post type. Following this tutorial, you’ll be able to build a grid of any post type.
Our final block will be displayed as a screenshot below.
As you can see in a screenshot, we’re also going to provide settings for the post grid block. This way you’ll have more control over the output and also you’ll learn how to use Inspector Control in your block.
With the help of Inspector Control, you can add a settings sidebar to your block. The Inspector Control is a wrapper for the fields going to be placed in the sidebar.
Getting Started
First, let’s define attributes to hold the values of the fields present in the sidebar. With these fields, we can manage the layout of the post grid.
{
...
...
"name": "artisansweb/post-grid",
"title": "Artisans Web - Post Grid",
"description": "This block will create a Post Grid.",
"attributes": {
"number_of_posts": {
"type": "string",
"default": "3"
},
"show_image": {
"type": "boolean",
"default": true
},
"show_title": {
"type": "boolean",
"default": true
},
"show_excerpt": {
"type": "boolean",
"default": true
},
"show_readmore": {
"type": "boolean",
"default": true
}
},
}
The attributes added here are quite self-explanatory. By default, we’re fetching 3 posts, the user can obviously change this value from the settings sidebar. For the other attributes, I used a boolean type to control whether to show a specific value or not(post title, excerpt, image, and a read more link).
Next, let’s define the <PostsList>
component in edit.js
:
function PostsList( { hasResolved, posts, attributes } ) {
if ( !hasResolved )
return <Spinner />;
if ( !posts?.length )
return <div>No posts to display</div>;
return (
<div className='post-grid'>
{ posts?.map( (post) => {
const { mediaId, media } = useSelect( select => {
return {
mediaId: post.featured_media,
media: select('core').getMedia(post.featured_media)
}
}, [post.featured_media] )
return (
<div className='post' key={ post.id }>
{ attributes.show_image && !! mediaId && ! media && <Spinner /> }
{ attributes.show_image && !! media && media && <img src={ media.media_details.sizes.thumbnail.source_url } /> }
{ attributes.show_title && <h3>{ post.title.rendered }</h3> }
{ attributes.show_excerpt && <p>{ post.excerpt.raw }</p> }
{ attributes.show_readmore && <a className='read-more'>Read More</a> }
</div>
)
} ) }
</div>
);
}
This component requires some props which we’ll pass later. What this component does – it loops through the posts array and prints the post grid. While displaying the grid, it checks for boolean values and prints the specific markup only if the attribute’s value is set to true.
Take note, I fetched the featured image of each post in the loop with the help of useSelect
hook. This is because, in the posts array(which will fetch in the next step), we’ll not have the URL of the post thumbnail. Instead, WordPress keeps the attachment id of the thumbnail. So here, using this id I’m fetching the actual image.
To get a look of the post-grid layout, add the following code to the editor.scss
file.
.wp-block-artisansweb-post-grid {
$post-grid-columns: 3;
$post-gutter: 20px;
.post-grid {
display: grid;
grid-template-columns: repeat($post-grid-columns, 1fr);
grid-gap: $post-gutter;
padding: $post-gutter;
}
.post {
border: 1px solid #ddd;
padding: 20px;
text-align: center;
img {
max-width: 100%;
height: auto;
margin-bottom: 10px;
}
h3 {
font-size: 1.2rem;
margin: 10px 0;
}
p {
font-size: 1rem;
}
.read-more {
background-color: #007bff;
color: #fff;
border: none;
padding: 5px 10px;
text-decoration: none;
transition: color 0.3s ease;
}
}
}
}
Fetch Posts In Block
In order to fetch posts, I’ll use the useEntityRecords
hook. This React hook returns an object with four properties:
hasResolved
: Boolean. It means records are fetched.isResolving
: Boolean. The request is in progress.records
: Array. The final result.status
: String. Any one value from – Resolving, Success, Error, Idle.
Out of these four properties, we’ll require hasResolved
and records
. Let’s do it as follows.
import { useEntityRecords } from '@wordpress/core-data';
import { BaseControl } from '@wordpress/components';
export default function Edit({ attributes, setAttributes }) {
const { hasResolved, records } = useEntityRecords( 'postType', 'post', { per_page: attributes.number_of_posts} );
return (
<div { ...useBlockProps() }>
<BaseControl label={__("Artisans Web - Post Grid")}>
<PostsList hasResolved={ hasResolved } posts={ records } attributes={attributes} />
</BaseControl>
</div>
);
}
Remember, a React hook must be called at the top level of the component(we called it in the first statement). You can’t call this hook within conditional or loops.
Here, I set the post
as a second argument of useEntityRecords. You can set any other post type’s name as per your needs. We are also passing a few props to the <PostsList/>
components defined earlier.
Inspector Control
We’re done with getting posts into the editor dynamically. Now, as mentioned earlier we have to add the settings sidebar to our block. In the settings sidebar, I’ll use <TextControl/>
and <ToggleControl/>
components to manage the settings.
So, after adding the settings sidebar, the final edit.js
will be as follows.
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import { useEntityRecords } from '@wordpress/core-data';
import { useSelect } from '@wordpress/data';
import { BaseControl, Spinner, ToggleControl, TextControl, PanelBody, PanelRow } from '@wordpress/components';
import './editor.scss';
export default function Edit({ attributes, setAttributes }) {
const { hasResolved, records } = useEntityRecords( 'postType', 'post', { per_page: attributes.number_of_posts} );
return (
<>
<InspectorControls>
<PanelBody title="Post Grid Settings" initialOpen={ true }>
<PanelRow>
<TextControl
label="Number of Posts"
onChange={ ( number_of_posts ) => setAttributes( { number_of_posts } ) }
value={ attributes.number_of_posts }
/>
</PanelRow>
<PanelRow>
<ToggleControl
label="Show Image"
onChange={ () => setAttributes( { show_image: ! attributes.show_image } ) }
checked={ attributes.show_image }
/>
</PanelRow>
<PanelRow>
<ToggleControl
label="Show Title"
onChange={ () => setAttributes( { show_title: ! attributes.show_title } ) }
checked={ attributes.show_title }
/>
</PanelRow>
<PanelRow>
<ToggleControl
label="Show Excerpt"
onChange={ () => setAttributes( { show_excerpt: ! attributes.show_excerpt } ) }
checked={ attributes.show_excerpt }
/>
</PanelRow>
<PanelRow>
<ToggleControl
label="Show Read More"
onChange={ () => setAttributes( { show_readmore: ! attributes.show_readmore } ) }
checked={ attributes.show_readmore }
/>
</PanelRow>
</PanelBody>
</InspectorControls>
<div { ...useBlockProps() }>
<BaseControl label={__("Artisans Web - Post Grid")}>
<PostsList hasResolved={ hasResolved } posts={ records } attributes={attributes} />
</BaseControl>
</div>
</>
);
}
function PostsList( { hasResolved, posts, attributes } ) {
if ( !hasResolved )
return <Spinner />;
if ( !posts?.length )
return <div>No posts to display</div>;
return (
<div className='post-grid'>
{ posts?.map( (post) => {
const { mediaId, media } = useSelect( select => {
return {
mediaId: post.featured_media,
media: select('core').getMedia(post.featured_media)
}
}, [post.featured_media] )
return (
<div className='post' key={ post.id }>
{ attributes.show_image && !! mediaId && ! media && <Spinner /> }
{ attributes.show_image && !! media && media && <img src={ media.media_details.sizes.thumbnail.source_url } /> }
{ attributes.show_title && <h3>{ post.title.rendered }</h3> }
{ attributes.show_excerpt && <p>{ post.excerpt.raw }</p> }
{ attributes.show_readmore && <a className='read-more'>Read More</a> }
</div>
)
} ) }
</div>
);
}
As you can see, I placed the components <TextControl />
and <ToggleControl />
within the InspectorControls
, PanelBody
, and PanelRow
. These are the wrapper for our fields.
To the PanelBody
, I set initialOpen={ true }
which will keep our setting open once someone enters the block.
You’re now ready with the backend part of the post grid block. Next, let’s render the output in the front end.
Post Grid On Front End
We’ve already stored the attributes of the block. Next, with the help of these attributes and WP_Query class, you can easily render the front end.
src/render.php
<?php
$args = array(
'post_type' => 'post',
'posts_per_page' => $attributes['number_of_posts']
);
$posts = new WP_Query( $args );
if ( $posts->have_posts() ) {
$post_list = '<div class="post-grid">';
while ( $posts->have_posts() ) {
$posts->the_post();
$post_list .= '<div class="post">';
if ( $attributes['show_image'] ) {
$post_list .= wp_get_attachment_image(get_post_thumbnail_id(get_the_ID()), 'medium');
}
if ( $attributes['show_title'] ) {
$post_list .= '<h3>'. '<a href="'. get_permalink() .'">'. get_the_title() .'</a>'.'</h3>';
}
if ( $attributes['show_excerpt'] ) {
$post_list .= '<p>'. get_the_excerpt() .'</p>';
}
if ( $attributes['show_readmore'] ) {
$post_list .= '<a class="read-more" href="'. get_permalink() .'">Read More</a>';
}
$post_list .= '</div>';
}
$post_list .= '</div>';
} else {
$post_list = 'No posts to display.';
}
wp_reset_postdata();
?>
<div <?php echo get_block_wrapper_attributes(); ?>>
<?php echo $post_list; ?>
</div>
Finally, to keep parity between the back end and front end, add the following code to the style.scss
file.
.wp-block-artisansweb-post-grid {
$post-grid-columns: 3;
$post-gutter: 20px;
.post-grid {
display: grid;
grid-template-columns: repeat($post-grid-columns, 1fr);
grid-gap: $post-gutter;
padding: $post-gutter;
}
.post {
border: 1px solid #ddd;
padding: 20px;
text-align: center;
img {
max-width: 100%;
height: auto;
margin-bottom: 10px;
}
h3 {
font-size: 1.2rem;
margin: 10px 0;
}
p {
font-size: 1rem;
}
.read-more {
background-color: #007bff;
color: #fff;
border: none;
padding: 5px 10px;
text-decoration: none;
transition: color 0.3s ease;
}
}
}
On the front end, I used the same CSS from the editor.scss
file. Feel free to change the styling of the front end as per your requirement.
If you liked this article, then please subscribe to our YouTube Channel for video tutorials.