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. 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’;

  2. 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 😉

  3. 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:)

  4. 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.

  5. 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.

  6. 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

            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.

          • 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 use global $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 Settings
            5. 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 had get_query_var( 'page' )

  7. 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!

  8. 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.