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 );
Lindsey says
Great tutorial, thank you!
Jo Waltham (@MagentaSkyUK) says
Thank you for this. I’ve been wondering how to make genesis_grid_loop responsive so that on mobile phones it goes to one column. It will be easier this way. Won’t it?
Bill Erickson says
Yep! The column classes are percentage based, so everything will scale down proportionally as the site gets smaller (assuming the rest of your site is responsive). Then in a media query you can say something like “at 600px wide, .one-third goes 100% wide and doesn’t float”. Here’s what I’m using in my gallery theme: https://gist.github.com/2046892
Mario says
That’s great, but I’ve encountered another problem. In fact, even if I can show the grid columns in full size, the images of grid posts looks very bad on mobile since they are displayed in a lower resolution than featured posts. So, my question is: is it possible to dynamically change the loop in order to show only featured images on mobile while keeping the columns on desktop homepage?
Bill Erickson says
You can use the wp_is_mobile() conditional to test whether you’re on a mobile device or not. Something like
$image_size = wp_is_mobile() ? ‘featured-mobile’ : ‘featured’;
Mario says
I forgot to say thank you, it worked like a charm. 🙂
Ahmad says
Great tutorial.
At first I thought this will be a custom post type for gallery, but it’s only for archieve.
Tried searching for tutorial on building a custom post type (portfolio/gallery), specifically coded for genesis, but couldn’t find one. Hopefully you’ll be able to post a tutorial on that if you’ve some spare time 😉
Bill Erickson says
Here’s a tutorial I wrote on custom post types.
Paul says
I knew there was a better way!
Adam W. Warner says
Very cool Bill, I’ve been using the grid loop for some time and as a non-programmer I’ve struggled.
Is there any opportunity to roll this into a plugin with associated options on where to apply this grids? I’d be happy to donate to the development of this plugin:)
Steve Hill says
Great post thanks Bill; so simple to use.
I have a slight problem with the second part of the tutorial – changing the number of posts shown in the query. I’m using the code in an Archive template for a Custom Post Type and this part doesn’t seem to be working for that. Would you be able to let me know how I can adjust this code to account for the CPT?
Thanks.
Bill Erickson says
Make sure you’re using the default loop (yoursite.com/cpt) and that the cpt had ‘has_archive’ => true, when registering it.
Place the query modification function in functions.php (required for any query modification). Then just change the conditional to is_post_type_archive( ‘cpt’
). Of course change cpt to your post type name.
GaryJ says
Well that’s considerably simpler than the current grid loop tutorials. Makes you wonder why the grid loop in Genesis exists in the first place (I guess, when you want a *second* loop running on the page, or at least one that’s different from what the page query args would otherwise be).
Bill Erickson says
I use the same technique for custom queries, except I manually specify the output with the classes rather than use the filter (I don’t run any of the actions in my custom loops).
Personally I think the grid loop and query_args custom field should go away, but for legacy reasons we should just deprecate it and recommend better approaches.
Bruce M says
Hi Bill,
I’m having trouble making this work with a CPT. I have this in functions.php https://gist.github.com/2140665 and this in my page template (page-books.php) https://gist.github.com/2140697
The query runs, and I don’t see any issues in debug, but it returns nothing to the page. I have quintuple checked my post type name, etc. and the normal grid loop runs fine if I use that instead. But I need to make your method work, because I am having the dreaded pagination issue with custom taxonomies with the grid loop.
Using Genesis (not that that would make any difference).
I will gladly pay you to help me with this.
Thanks!
Bill Erickson says
Don’t use a page template, use WordPress’ default archive for the post type.
– When you register the post type, make sure
'has_archive' => true– Go to Settings > Permalinks and click “Save”
– Rename your file archive-wps_books.php
You’ll be able to access the archive at yoursite.com/wps_books.php . To get a cleaner URL, add this to your register_post_type function:
'rewrite' => array( 'slug' => 'books' )Using a page template is NOT using the default query, since the default query in that case is the page. is_post_type_archive() will return false because the main query is a page, not a post type archive.
If you must use a post type archive (ex: want to control the text at the top of the page, or this is just a secondary query on the page), then manually build the markup for the custom query. You don’t want to use post_class() as that should be reserved for the main query. Example: https://gist.github.com/2140818
Bruce M says
Bill!
That worked great. My ignorance on main query versus page. I’m a bit new at this, and have struggled for DAYS trying to understand it.
Thank you VERY much for your help.
I’ll be hanging on my chair watching for your next post 🙂
Thanks again!
Bill Erickson says
No worries, it is a difficult concept to grasp because no one ever talks about it, and you can do so much with WordPress without completely understanding what’s going on under the hood. It wasn’t until recently that I really understood it well, which is why I try to help others figure it out faster 🙂
Bruce M says
It’s Awesome. Say good by grid loop 🙂
Did I say THANK You? 🙂
Bruce M says
Bill,
I don’t have any pagination, but I assumed that the ‘posts_per_page’ => 12 arg would handle that?
Do you have any idea why I wouldn’t have pagination (controls do not show up at the bottom).
Default number of posts in WP is set to 10.
Thanks.
Bruce M says
Hi Bill,
Any idea why I wouldn’t have pagination? Here is my code for my page template https://gist.github.com/2155009
I’ve tried several things and don’t see what is wrong.
Thanks!
Bill Erickson says
Have you tried adding the pagination function, genesis_posts_nav(), after your endwhile? https://gist.github.com/2158296
Genesis adds it automatically to the default loop but you’ll need to add it to your custom one. But even this might not work because its a page template and somewhere in there I think it checks to make sure is_singular() == false before it displays pagination.
On my gallery, the “Galleries” link originally was a page template that listed all posts, but I couldn’t get it to paginate either. So I ended up just making a category called “All” that I put everything in, and just linked to that (so it would be using the main query, and pagination would work).
Bill Erickson says
Actually, after posting that it bugged me enough to dig in and see exactly what was happening. I’ve figured out how to do pagination on a custom loop:
1. Remove the default pagination:
remove_action( 'genesis_after_endwhile', 'genesis_posts_nav' );2. Build your custom loop $args, and include
'paged' => get_query_var( 'paged' )3. Overwrite wp_query with your new one. So instead of
$loop = new WP_Query( $args );, you useglobal $wp_query; $wp_query = new WP_Query( $args );Since all the conditional tags like is_singular() are really a wrapper for $wp_query->is_singular(), this changes all conditional tags to be based off of your custom query.4. After your endwhile, place
genesis_posts_nav();. This will render the navigation you have set in Genesis > Theme Settings5. After your endif; place
wp_reset_query(). This will revert $wp_query and $post to the original query.Here’s an example with it all in place: https://gist.github.com/2158395
Bruce M says
Hi Bill,
That worked in getting the pagination, but with one small problem. When paging to the next page, it displays the same 6 posts (I have post_per_page =>6) on EVERY page…??
Actually the 6 most recent…
Any idea why that would be???
If YOU had a tough time with the pagination, I NEVER would have found the solution! 🙂
(I tried several different methods and couldn’t make it work)
Thanks MUCH!
Bill Erickson says
Make sure you have
'paged' => get_query_var( 'paged' ). In your previous code you hadget_query_var( 'page' )Bruce M says
Wow, that was it…
I had read that ‘page’ was the proper way, but that was with WP_Query –
$query = new WP_Query( ‘paged=’ . get_query_var( ‘page’ ) );
http://codex.wordpress.org/Class_Reference/WP_Query#Post_.26_Page_Parameters
Sooooo grateful for your help Bill. I really did not want to turn this into a ‘teach the noob the code fest’ 🙂
Thank you sir!
P.S. – the new avatar is very… different 🙂
Steve says
Wow – infinitely simpler than the grid loop. One question though, and it’s not a game changer. In the grid loop you can specify a number of full width posts, and a number of grid posts. With this code you specify the number of columns only.
If I want to add a full width (one column) post I can add a widget easy enough, but is there a way to specify that in this code, or add another grid loop?
Excuse my ignorance if this doesn’t make much sense. I’m new to coding, period! 🙂
Steve
Bill Erickson says
Yes, this is definitely doable – I actually just finished doing it for a project (site’s still under construction so can only provide a screenshot: http://twitpic.com/900pbd )
Here’s part of the code I’m using. I’m giving all the full width posts a class of ‘feature’ and the grid posts a class of ‘teaser’. I’m also alternating between ‘left’ and ‘right’ classes on the teasers. Finally, I’m adding a class of ‘last’ to the last post.
On a simpler site you could do it all with column classes instead of the left, right, and last class. It’s just that the design I’m working with is pretty complex and I didn’t want to mess with the CSS of the column classes.
A big benefit to this approach is that you can then target code to posts based on their post classes. In this site (and provided in that code snippet), I’m doing a different post meta on teasers.
Steve says
That’s good to know. So, if I were using this on the front page only, would I use this code but add a conditional is_singlular statement?
I will play with the code, but right now it’s not working so I know I’m missing something.
Thanks for the pointers though – this is the fun part! 🙂
Steve
Bill Erickson says
You could put this in functions.php and add a bunch of && !is_singular() conditionals to leave them off the single posts and pages. But I like to packaged all relevant code together in a single file.
– I created archive.php which contains all of this ( https://gist.github.com/2166989 )
– In functions.php, I wrote a function that points home, archives, and search to this file. This way I didn’t have to duplicate the code in home.php and search.php ( https://gist.github.com/2166998 )
That’s about as much code as I can give you now. I gotta finish this project!
Steve says
I’m grateful for you answering the post, let alone giving me a head start on the code. 🙂 I really appreciate the help / pointers, and am going to leave you alone now! 🙂
I’m going to try this out tomorrow – thanks a ton for the guidance. I’ll let you know how I get on!
Steve
Steve says
Just wanted to let you know the code worked like a charm. I’m am VERY grateful for your help. I have a greater understand of why / how it works, and that is priceless also.
Thanks again for this, and for all the other tutorials / code snippets you’ve given.
Steve
Bill Erickson says
No worries, I’m glad it worked for you! We’re all learning this together so as you figure things out, contribute them back.
If you look at my Code Snippets you can see I was messing around with grid loops and author boxes last night. I got my grid loop working as well!
Bruce M says
Bill,
I hate to ask again, but now that I have pagination working in a page template, I can’t get it working in taxonomy template page. I’ve tried many variations. Everything works except for pagination, which gives me a 404.
It’s probably something stupid on my part…
Here’s my code for taxonomy-wps_genre.php https://gist.github.com/2173670
Thanks, and I promise I’ll leave you alone after this 🙂
Bill Erickson says
In my post I describe why you need to break up what to display (the query) with how to display it. If you need to make changes to the default query (ex: use 6 posts per page), then customize the main query.
Bruce M says
Hi Bill,
Finally got my head wrapped around the main query, and the use of templates, and got it working. To share and help others, here is what I did:
1) Added the Post Class code from your Step 1 above, to my taxonomy-yourtaxonomy.php template.
2) Added the query customization code from Step 2 above, to my FUNCTIONS.php (this is the piece I kept confusing with my previous usage in a page template on a cpt; I was incorrectly putting it in the taxonomy template file).
My issue was I was confusing the page template/custom loop side with the fact that I could just let the main query and WP native functionality take care of the taxonomy piece…
Works great now, and I learned a lot!
Thanks again.