A better, and easier, grid loop

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 );

Bill Erickson

Bill Erickson is the co-founder and lead developer at CultivateWP, a WordPress agency focusing on high performance sites for web publishers.

About Me
Ready to upgrade your website?

I build custom WordPress websites that look great and are easy to manage.

Let's Talk

Reader Interactions

Comments are closed. Continue the conversation with me on Twitter: @billerickson

Comments

  1. Paul says

    how do you deal with sticky posts? they don’t count as part of the main loop it seems. so if I define the number of posts to display as 12, and there’s a sticky post, it breaks the logic.
    This is in the context of a public release theme where I can’t control whether a user will use the sticky functionality or not.

    • Bill Erickson says

      To be honest I never use, or recommend, sticky posts. It’s an outdated feature in WordPress.

      But since you’re releasing it publicly and can’t simply tell people “don’t do it”, I’d take the following approach:
      – Hook right before the main loop, and if on the blog homepage do a query for just sticky posts.
      – Keep a global post counter going of posts used. Base your grid loop on this rather than the main query’s post counter.
      – Use pre_get_posts to remove sticky posts from the main loop ( $query->set( 'post__not_in', get_option( 'sticky_posts' ) );)
      – Use pre_get_posts to offset posts. If on the first page, limit posts to post count – sticky posts (if 12 post count and 4 sticky posts, 8 normal posts). If on any other page, offset the posts by number of sticky posts + posts per page * (page number – 1).

      It’s a big hassle, but basically rebuild the main query so, if sticky posts are in use, it doesn’t mess up the post count.

  2. Elly Lammers says

    I really like this simple grid loop. Only problem I have with it is that it’s also splitting the featured page widget in two. How can I prevend that? The code if( is_singular() )
    return $classes; is in it. Is there a code to skip the featured post and page widget? Or am I the only one with this problem?

    • Bill Erickson says

      That’s a good question. I don’t use the featured page/post widget so haven’t had that issue.

      I don’t have time to figure out a solution right now, but I can hopefully point you in the right direction. The queries should be different, so in your archive post class function, start it out with something like: https://gist.github.com/2577723

      If that doesn’t work, hook into genesis_before_post_content, run this: https://gist.github.com/2577727 That will show you all the values of $wp_query in every post (both your main loop and your featured page widget loop). Look for anything that’s different between them that you could use.

    • Mike says

      I’d love to know if this was worked out, and how…

      I tried the suggested solution, but it did not resolve it. I love this grid layout, but also want to keep the featured posts widget.

  3. Affan Ruslan says

    Thanks for the tutorial Bill.

    Quick question, if I would like to use it sidewide, I know I’ve to paste the last code into my functions.php file.

    How do I specify which size of image will be use? Let say I register new size of image, like

    add_image_size( ‘home_image’, 160, 113, true );

    How do I specify that image to be used in the grid?

    • Bill Erickson says

      Go to Genesis > Theme Settings > Content Archives and check “Include Featured Image”, and select the image size.

      If you have features (full width posts) and teasers (the two columns of posts), I like to do something like this ( https://gist.github.com/3172761 ).

      Genesis has a nifty feature in the genesis_get_option function that lets you to hop in before an option is called and modify it. Before Genesis displays your featured image, it checks the theme setting to see what image size you want to use. That above code checks to see if you have a post class of ‘one-half’ (which means this is a teaser) and if it does it uses the smaller image.

      • Affan Ruslan says

        Nice one Bill, now I got it.

        Last question, I see from the screenshot above (your gallery theme), the title is above your picture. However, I can see from your current gallery that you swap the position of title and image.

        How do I do that? (I’m asking since I couldn’t find the code from the github link you provided above. Most probably that one was the earlier version of the theme? 😀 )

  4. Phil says

    hi Bill,

    I was trying the SITE WIDE option which displays the posts in 3 columns – works really well.

    as an experiment I adjusted the code you had to try to show 2 columns instead [by using the class one-half instead of one-third and changing the current post query to 2 instead of 3]

    seemed to work except the excerpt text seemed to be wrapping along the right side of the thumbnail images instead of sitting neatly below the images as was the case with 3 columns.

    how can i fix it so it works for 2 columns instead of 3?

    thanks

    Phil

    • Bill Erickson says

      I’d have to see the site to provide specific styling, but my guess is that the image size you have is smaller than the width of the column. Genesis makes the post image float left, so if the image is less wide than the column the text will float to the right of it. You could override it with something like this: https://gist.github.com/3298352

      • Phil says

        Thanks Bill – you were right – image was slightly smaller than the column width.

        I added in the CSS tweak and it worked a treat

        cheers

        Phil

  5. Affan Ruslan says

    Thanks for adding the Advance Example.
    When you said three column, do you mean three column with a sidebar, or three column full width? Or it doesn’t matter since it uses percentage?

    • Bill Erickson says

      It doesn’t matter. It’s three columns within the #content column. Your site might be full width ( just a #content column), two columns ( #content and #sidebar) or three columns ( #content, #sidebar, and #sidebar-alt).

      If you take a look at the Column Classes CSS, you’ll see that .one-third makes something 31% the width of its containing element

  6. Jesse says

    This seems to mess the pagination up. It makes it think there are way more pages than there really are when you use numeric pagination. Does that make sense? Any ideas what needs changed?

    • Bill Erickson says

      There shouldn’t be any way to mess up the pagination using this technique. That’s one of the main reasons to use this technique over the Genesis’ grid loop, which does mess up pagination.

      Can you post the code you’re having an issue with?

    • Bill Erickson says

      How many posts do you have? The math suggests 758-767 posts.

      One of the reasons we modify the main query in be_grid_loop_query_args() is so that the pagination is correct.

  7. Jesse says

    Nope, 537. For some reason that query is making the pagination think there are way more posts.

    • Bill Erickson says

      I recommend digging into the numerical pagination function and see where that number is coming from. It could be a bug with my code (although I haven’t seen that issue before), an issue with some other code running on your site in your theme or plugins, or a bug in a WP core function.