Don’t feel like coding? I built a plugin that does this for you.
See the Genesis Grid Loop plugin.
Note: This technique can be used by all WordPress themes. I’m proposing it as a replacement for the Genesis-specific grid loop, so aspects of this post might be Genesis-specific.
A popular request is to list posts in multiple columns. I do it on my blog, and do it often on my clients’ sites.
Genesis developed a Grid Loop, which you can utilize inside your Genesis themes for this effect, and is how this blog does it. While very useful, it can be difficult to set up. I believe this is because they combined two separate functions: what content to display (your query) and how to display it.
By breaking those two functions we can use another feature of Genesis, the column classes, to build grid loops easier. You can also copy that CSS to any WordPress theme and make it work too.
For this example I’ll be using a Gallery theme I recently built. On the archive pages, it displays posts in three columns. See the screenshot above, or click here) for an example.
Step 1: Multiple columns using post_class
The post_class
filter lets us customize the classes applied to each post in the loop. Since I want this to only apply to archive pages, I’m placing it in archive.php.
/**
* Archive Post Class
* @since 1.0.0
*
* Breaks the posts into three columns
* @link http://www.billerickson.net/code/grid-loop-using-post-class
*
* @param array $classes
* @return array
*/
function be_archive_post_class( $classes ) {
global $wp_query;
if( ! $wp_query->is_main_query() )
return $classes;
$classes[] = 'one-third';
if( 0 == $wp_query->current_post % 3 )
$classes[] = 'first';
return $classes;
}
add_filter( 'post_class', 'be_archive_post_class' );
The first line adds a class of ‘one-third’ to all posts. Then I grab the current post counter out of $wp_query
, and if this is the first post ( 0 == $wp_query->current_post
) or if the remainder of the current post divided by 3 is zero (this tells us the current post is the first in a row), apply a class of “first” as well.
That’s it! You now have your content broken into multiple columns. If you want two columns, use ‘one-half’ and divide the current post by 2. If you want four columns, use ‘one-fourth’ and divide the current post by 4.
Step 2: Customize the Query
When I view the archive page now, it’s in three columns but it’s only displaying 10 posts. The last post sits by itself in its own row. I’m going to modify the main query to show 27 posts per page. You could use any number you want, just make sure it’s a multiple of columns. For more information on customizing the main query, see this post.
/**
* Archive Query
*
* Sets all archives to 27 per page
* @link http://www.billerickson.net/customize-the-wordpress-query/
*
* @param object $query
*/
function be_archive_query( $query ) {
if( $query->is_main_query() && $query->is_archive() ) {
$query->set( 'posts_per_page', 27 );
}
}
add_filter( 'pre_get_posts', 'be_archive_query' );
This code must go in functions.php, since the main query runs before it reaches archive.php (it checks the query to figure out what template to load).
Even easier sitewide
If you’re using this for all listings of posts (home, archive, search…), it is even easier to set up. Put the post_class filter in functions.php so it runs sitewide, and add a conditional to check if it isn’t singular:
/**
* Archive Post Class
*
* Breaks the posts into three columns
* @link http://www.billerickson.net/code/grid-loop-using-post-class
*
* @param array $classes
* @return array
*/
function be_archive_post_class( $classes ) {
// Don't run on single posts or pages
if( is_singular() )
return $classes;
$classes[] = 'one-third';
global $wp_query;
if( 0 == $wp_query->current_post || 0 == $wp_query->current_post % 3 )
$classes[] = 'first';
return $classes;
}
add_filter( 'post_class', 'be_archive_post_class' );
And instead of the function to customize the number of posts, go to Settings > Reading and tweak it there.
Advanced Example
The code snippet below (added to functions.php), modifies the blog’s homepage and archive pages to display 5 features and 6 teasers (in three columns) on the first page. On inner pages, it displays 0 features and 12 teasers (in three columns). It also updates the post image to use image sizes specifically created for features and teasers.
The first function, be_grid_loop_pagination()
, is where we control the grid loop. Under the comment that says “Sections of site that should use grid loop”, you can modify that list to specify where you want the grid loop displayed. Right now it is running when is_home()
or is_archive()
is true. The second part, under the comment that says “Specify pagination”, is where you specify how many features and teasers to show on the homepage and subsequent pages.
The second function, be_grid_loop_query_args()
, doesn’t require any customization from you. It uses the pagination information you added to the previous function to tell WordPress how many posts show up on each page.
The third function, be_grid_loop_post_classes()
, applies relevant classes to each post. It’s adding a class of ‘feature’ to each feature post, and column classes to the teasers. The only thing that you’d need to change is the teasers sections if you want something other than three columns. Here’s what you’d need to change if you wanted two columns.
The fourth function, be_grid_image_sizes()
, specifies the two image sizes. Change these to whatever size you’d like. If you don’t want images, you can leave out this function and the last one, be_grid_loop_image()
. Also note that you need to have “Include Featured Image” checked in Genesis > Theme Settings > Content Archives.
The fifth function, be_grid_loop_image()
, overrides the image size set in Genesis > Theme Settings > Content Archives with the image sizes we created in the previous function. No changes need to be made to this function.
The sixth function, be_fix_posts_nav()
does as its name implies. The post navigation (Older/Newer, numerical links to posts pages…) uses $wp_query->max_num_pages
to know how many pages there are, and this is based on the current page’s posts_per_page. So if you have less posts on your homepage than inner pages, the post navigation on the homepage will be off (this is noticeable if you’re using numerical links). This code changes the max_num_pages based on the grid args.
/**
* Grid Loop Pagination
* Returns false if not grid loop.
* Returns an array describing pagination if is grid loop
*
* @author Bill Erickson
* @link http://www.billerickson.net/a-better-and-easier-grid-loop/
*
* @param object $query
* @return bool is grid loop (true) or not (false)
*/
function be_grid_loop_pagination( $query = false ) {
// If no query is specified, grab the main query
global $wp_query;
if( !isset( $query ) || empty( $query ) || !is_object( $query ) )
$query = $wp_query;
// Sections of site that should use grid loop
if( ! ( $query->is_home() || $query->is_archive() ) )
return false;
// Specify pagination
return array(
'features_on_front' => 5,
'teasers_on_front' => 6,
'features_inside' => 0,
'teasers_inside' => 12,
);
}
/**
* Grid Loop Query Arguments
*
* @author Bill Erickson
* @link http://www.billerickson.net/a-better-and-easier-grid-loop/
*
* @param object $query
* @return null
*/
function be_grid_loop_query_args( $query ) {
$grid_args = be_grid_loop_pagination( $query );
if( $query->is_main_query() && !is_admin() && $grid_args ) {
// First Page
$page = $query->query_vars['paged'];
if( ! $page ) {
$query->set( 'posts_per_page', ( $grid_args['features_on_front'] + $grid_args['teasers_on_front'] ) );
// Other Pages
} else {
$query->set( 'posts_per_page', ( $grid_args['features_inside'] + $grid_args['teasers_inside'] ) );
$query->set( 'offset', ( $grid_args['features_on_front'] + $grid_args['teasers_on_front'] ) + ( $grid_args['features_inside'] + $grid_args['teasers_inside'] ) * ( $page - 2 ) );
// Offset is posts on first page + posts on internal pages * ( current page - 2 )
}
}
}
add_action( 'pre_get_posts', 'be_grid_loop_query_args' );
/**
* Grid Loop Post Classes
*
* @author Bill Erickson
* @link http://www.billerickson.net/a-better-and-easier-grid-loop/
*
* @param array $classes
* @return array $classes
*/
function be_grid_loop_post_classes( $classes ) {
global $wp_query;
// Only run on main query
if( ! $wp_query->is_main_query() )
return $classes;
// Only run on grid loop
$grid_args = be_grid_loop_pagination();
if( ! $grid_args || ! $wp_query->is_main_query() )
return $classes;
// First Page Classes
if( ! $wp_query->query_vars['paged'] ) {
// Features
if( $wp_query->current_post < $grid_args['features_on_front'] ) {
$classes[] = 'feature';
// Teasers
} else {
$classes[] = 'one-third';
if( 0 == ( $wp_query->current_post - $grid_args['features_on_front'] ) || 0 == ( $wp_query->current_post - $grid_args['features_on_front'] ) % 3 )
$classes[] = 'first';
}
// Inner Pages
} else {
// Features
if( $wp_query->current_post < $grid_args['features_inside'] ) {
$classes[] = 'feature';
// Teasers
} else {
$classes[] = 'one-third';
if( 0 == ( $wp_query->current_post - $grid_args['features_inside'] ) || 0 == ( $wp_query->current_post - $grid_args['features_inside'] ) % 3 )
$classes[] = 'first';
}
}
return $classes;
}
add_filter( 'post_class', 'be_grid_loop_post_classes' );
/**
* Grid Image Sizes
*
*/
function be_grid_image_sizes() {
add_image_size( 'be_grid', 175, 120, true );
add_image_size( 'be_feature', 570, 333, true );
}
add_action( 'genesis_setup', 'be_grid_image_sizes', 20 );
/**
* Grid Loop Featured Image
*
* @param string image size
* @return string
*/
function be_grid_loop_image( $image_size ) {
global $wp_query;
$grid_args = be_grid_loop_pagination();
if( ! $grid_args )
return $image_size;
// Feature
if( ( ! $wp_query->query_vars['paged'] && $wp_query->current_post < $grid_args['features_on_front'] ) || ( $wp_query->query_vars['paged'] && $wp_query->current_post < $grid_args['features_inside'] ) )
$image_size = 'be_feature';
if( ( ! $wp_query->query_vars['paged'] && $wp_query->current_post > ( $grid_args['features_on_front'] - 1 ) ) || ( $wp_query->query_vars['paged'] && $wp_query->current_post > ( $grid_args['features_inside'] - 1 ) ) )
$image_size = 'be_grid';
return $image_size;
}
add_filter( 'genesis_pre_get_option_image_size', 'be_grid_loop_image' );
/**
* Fix Posts Nav
*
* The posts navigation uses the current posts-per-page to
* calculate how many pages there are. If your homepage
* displays a different number than inner pages, there
* will be more pages listed on the homepage. This fixes it.
*
*/
function be_fix_posts_nav() {
if( get_query_var( 'paged' ) )
return;
global $wp_query;
$grid_args = be_grid_loop_pagination();
if( ! $grid_args )
return;
$max = ceil ( ( $wp_query->found_posts - $grid_args['features_on_front'] - $grid_args['teasers_on_front'] ) / ( $grid_args['features_inside'] + $grid_args['teasers_inside'] ) ) + 1;
$wp_query->max_num_pages = $max;
}
add_filter( 'genesis_after_endwhile', 'be_fix_posts_nav', 5 );
Karan M says
Referring to the comment about having a min-height so that teaser posts in the grid are of equal heights – how do you maintain this when the window resizes? I have been trying to use display:flex but can’t for the life of me get it working correctly. Hoping for some help here.
Bill Erickson says
I like to use this jQuery match height library for making elements maintain equal heights. What’s nice is when your posts go full width, the min height gets removed.
Alejandro says
Hi! First of all thanks a lot, this is all i was looking for.
Everything is perfect, but i have the same 2 problems with pluging and code.
1- I set 11 entries and in blog page it show 12 and in category pages 11.
2- I added new image size but it take featured post image size anyways.
Thanks, Alejandro.
Bill Erickson says
1. Either your theme or a plugin is modifying the number of posts per page. I’d search your theme’s functions.php and home.php files for `posts_per_page`, and if you don’t find anything, start disabling plugins to see which one is causing it.
2. Images are generated when they are first uploaded. If you add a new image size, all the existing images won’t automatically be resized to it – only new images. You can fix this by using Regenerate Thumbnails.
martin allman says
Hi Bill
I wish to show 2 cols on my home page but 3 cols on my archive pages – could I use something like this:
global $wp_query;
$cols;
$colclass;
if($wp_query->is_home()) {
$cols = 2;
$colclass=’one-half’;
}
else{
$cols = 2;
$colclass=’one-half’;
}
if( ! $wp_query->is_main_query() )
etc etc
Ferlin says
Hi Bill
Gridloop code is working very well, thank you.
1 issue: For featured images added to the post, when I start to shrink the webbrowser down to 1 column (initial is 3 columns showing vertically) the featured image expands to a large size to the width of the 1 column (which is wider now since it’s only 1 column, not 3).
Is there a way to stop the columns reducing from 3 to 1 until the browser window is very small? or another solution to this? I have regenerate thumbnails added, no effect as I can see.
Thanks
Bill Erickson says
The column class CSS Genesis includes goes to 1 column at 767px. You can edit the media query to make this a lower number.
Ferlin says
Thanks Bill, that was it. Had to change the genesis core style.css. Thanks again for the code.
Hoang says
Hi Bill, is it possible to insert content after first row of the grid? How to do it please?
Thank you.
Bill Erickson says
Yes you can. Use the
genesis_before_entry
hook and look at$wp_query->current_post
to know your position in the loop.If you have a three column grid, you could use this code to insert a call-to-action div before the fourth post (counting starts at 0 so the fourth post is
current_post == 3
)https://gist.github.com/billerickson/62f13c58873f893d394283719c90a030
chris says
Great tutorial! Is it possible to use this to show pages instead of posts?
Bill Erickson says
The code above customizes the WordPress archive pages to display a grid. WordPress doesn’t have archive pages for the “page” post type.
That said, you could use a similar approach in a custom template to list pages. When outputting your custom loop, simply use the column classes (ex: .one-half, .one-third) around your pages to display them in columns.
Mos says
Thanks for this great content! It always helps me a lot!
Would you maybe know how I could add in banners (same size as posts) in the grid.
Let’s say I want to add in a banner at the first and sixth spot, replacing the blogpost area.
So first row it shows a cta-banner/image and two blogposts, second row two blogposts and then a ctabanner/image.
Hope it’s clear what I’m trying to ask here.. 😉
Thanks!
Bill Erickson says
The way to insert content in the middle of the listing completely depends on your theme. Hopefully your theme uses hooks, in which case you can use the hook that runs before the post is listed and display your banner based on the current post count.
If you are using Genesis, this code will insert a “cta-banner” div before the first and fifth posts: https://gist.github.com/billerickson/ab5c2fb4943be8520937b026f73bd2d5
Lynsey says
The pagination in this is all messed up, it jumps to the top and sometimes the middle of the blog posts
Bill Erickson says
Thanks! I just disabled the smooth scrolling on the comment pagination links. It will still use smooth scroll when the next page of comments loads to take you down to the comment section.
Pamela says
Hi Bill, this is exactly what I needed. However, I noticed that the post date is not showing on the blog homepage, but when I go to the second page using the pagination, the dates are showing. Is there a way for the dates to show on the homepage?
Bill Erickson says
I’m sorry but this code does not affect the display of your post date, it only changes the width of the post itself. It sounds like the post date issue is an issue in your theme.
Pamela says
Ok, yes, I did some research on a couple of different themes and some did the same with the post date, then some didn’t. So you are correct, it’s something in the theme that’s making the post date to not show up.. digging deep!!! Thanks so much for your reply! I really appreciate it!