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. Mario says

    Just to share, I managed in the end to create a separate mobile cache using W3 Total Cache. It’s very simple: Performance -> User Agents Group -> Enable the default “high” group containing all the mobile user agents, and it will create a separate cache only for them. 🙂

  2. AnitaC says

    Hi Bill, I am trying to assist another developer with the plugin version of your code. She’s getting this message:

    Warning: Division by zero in /nfs/c03/h07/mnt/52073/domains/HER-DOMAIN-NAME.com/html/wp-content/plugins/genesis-grid-loop/plugin.php on line 262

    She displaying the output so that only the featured image shows up – no titles or text, for her portfolio pages using categories. It looks great but we have that one error showing up on the last row. The plugin doesn’t give us any check boxes to eliminate the text and post info, so we are configuring that in the Genesis Settings.

    She’s displaying 4 across but the last row only has 3. In the spot where the 4th one would go – is that line of error code. Is there any way to fix this? I have started using the plugin on several things but this is the first with this error.

    Also if you plan to update, is there any way you might consider adding option boxes to use the content and post info?

    Thanks in advance.

    • Bill Erickson says

      Here is the line that error message is referencing: https://github.com/billerickson/Genesis-Grid/blob/master/plugin.php#L262

      That part of the code fixes the pagination in case you have a different number of posts on front page and inner pages. If you had 10 posts, homepage set to 2 posts per page and inner pages set to 8, then on homepage you’d see there’s 4 more pages of posts, and clicking to the next page you’d see there’s no more pages of posts. This corrects the issue.

      There’s only two ways I can see you getting a “Division by zero” issue.
      1) In Genesis > Grid Loop you have the features and teasers for the inner pages set to 0. Don’t do this, make sure there’s at least one post per inner page
      2) You’ve made some customization to the grid args using the 'genesis_grid_loop_args' filter and it’s caused issues.

      Can you take a look at your implementation and see if either of these are the case? If the second one, share the modifications you’ve made so I can look for errors.

      I don’t plan to add options to eliminate the content and post info. Genesis already provides you these options in Genesis > Theme Settings, so I think it would be confusing to duplicate these. Also, any modifications you’ve made to the theme (like moving post title or info to new location) would make it so I can’t remove them since I wouldn’t know where they are located, leading to a poor user experience. I’d get support issues like “I turned off post titles but they’re still showing up” and then look into their theme to see they’ve moved the post title.

      This plugin is supposed to provide a simple interface for breaking your posts down into a grid. No more, no less. Customizations to the output of your posts belong inside your theme, just like if you were customizing the output of the posts without a grid loop.

      I provide a helper filter that tells you if you’re in the grid loop or not: if( apply_filters( 'is_genesis_grid_loop', false ) ) You can then use this in your theme or core functionality plugin to make customizations that only apply to the grid loop

  3. AnitaC says

    Thank you for responding so quickly. I will run back over to the dashboard and also ask her about the changes she might have made to the code. I will let you know!

  4. Patrick McCoy says

    Hi Bill, as many have noted above, great tutorial!

    I just read through every comment/question and didn’t see the one specific question I have. As @Joy Waltham above asks about the responsive code moving to 1 column on mobile devices, my question pertains to getting the grid to show in a responsive rotator and not just a 1 column display.

    For example, if we have 4 columns, 4 rows of a gallery (16 images), can your code be modified to convert to a 16 image rotator on mobile devices, so the user will just swipe left/right to view rather than having to have a lengthy up/down scroll? The goal of this would be to eliminate the long up/down scroll of all those images when viewed on a smartphone. Any thoughts? Thanks in advance and hope my question makes sense.

    • Bill Erickson says

      I don’t think there would need to be any changes to the grid code, other than changing out the image sizes for mobile (you’ll want features and teasers to all have the same image size in the rotator). You’ll just need to write the proper CSS and jQuery to achieve the functionality you’re looking for.

      Or you could replace the whole content area if wp_is_mobile() with a Flexslider rotator containing the content of your posts.

  5. Amitabha says

    Hi Bill,
    Thanks for this great code. I was wondering for few days to have this grid view, but one question I have. Can it be built in a non-Genesis theme?

    • Bill Erickson says

      The basics apply to any theme. You can add the functions that filter post_class and pre_get_posts and this will work on non-Genesis themes. You’ll need to add the Column Class CSS to the theme’s stylesheet.

      The advanced example uses Genesis functions to customize Genesis’ output (ex: change image sizes), so that’s not applicable.

  6. David Alexander says

    I am trying to use this with the epic theme for a custom post type index which is created by a plugin.

    The post type is courses and thus creates an index of courses at site.com/courses however gaining control over this is difficult and creating an archive template for this using your above code just crashes it to a white screen. Any ideas?

  7. Mario says

    I’d like to modify the Genesis’ excerpt behaviour; specifically I would like it to behave in one of these two ways:

    1. Once the defined word limit is reached, show everything until it encounters ?, ! or . characters
    2. Show the first paragraph

    I’ve found this http://wordpress.stackexchange.com/questions/141125/allow-html-in-excerpt/141136#141136 which actually works like a charm for a normal WP theme, but not for Genesis. Indeed Genesis seems to generate its featured-posts excerpt in a different way from grid-posts and also the read-more text which you can actually modifying using the filter must be generated in a different way. So, my question is: do you have one of your great snippets to share in order to have a clean excerpt for the front-page? Thank you in advance.

  8. Bas says

    Hello Bill,

    At first I’d like to say thank you for all these awesome guides, scripts and snippets that you’ve put out.
    They definitely helped me on my way with tackling the thing called building my website.

    I’ve downloaded your plugin, and I’m wondering: Is it on any way possible to do the following things:

    – Move the title of a post below the thumbnail.
    – Make it so that the title only is below the thumbnail (So that if you have a long title that it will not continue until the next box)
    – Keep the excerpt of the featured/main post, but remove the excerpts of all non-featured posts

    I hope to hear from you,

    Thank you!
    Bas

    • Bill Erickson says

      Yes it is possible to style the featured posts and teasers however you like. Use this to determine if you’re currently in the grid loop (so your code doesn’t apply to non-grid looped posts):

      $in_grid = apply_filters( 'is_genesis_grid_loop', false );

      You can then look at the post_class() function to determine if it’s a teaser or feature. Ex:

      if( in_array( 'teaser', get_post_class() ) ) .. do teaser code

      For more guidance, see the snippets here: https://www.billerickson.net/code-tag/genesis-grid/

      • Bas says

        Hello Bill, thanks for your quick response!

        I managed to get the following things done:

        – Set excerpt length for featured post
        – Remove excerpt for teaser posts
        – Remove the “…” which you get once you set excerpt length to 0.

        This is the code that I fixed together, might not be “Best practice” (I don’t know that much about coding…at all) but it works.

        https://gist.github.com/billerickson/94b41059f7af13856898

        now to just figure out how to get the titels to show below the thumbnails and I’m done for that section 🙂

        • Bill Erickson says

          https://gist.github.com/billerickson/39b8e0aeb1465e3bdf4f

          You can of course strip out the unnecessary Genesis 1.x stuff assuming you’re using HTML5 in your theme.

          For future reference, go to /genesis/lib/structure/post.php to see where everything in the loop is hooked. If you want to move something you just remove_action() whatever you see in there, then add_action() it somewhere else.

          To see all available hooks, go to /genesis/lib/structure/loops.php. genesis_standard_loop() is used for HTML5, genesis_legacy_loop() is used for HTML4.

  9. Adam says

    Hey Bill,

    Thank you for the article. I have a question. Why not just enqueue the Unsemantic grid ( http://unsemantic.com/ ) and then modify the HTML divs with the appropriate classes inside a custom hook function? I’m new to Genesis and trying to figure it out.

    • Bill Erickson says

      You definitely could do that. But this post was designed for Genesis, and Genesis has its own built-in column classes. If you are enqueuing an alternative CSS framework then you can follow the tutorial here but use that framework’s CSS classes.

      Personally I don’t recommend using CSS frameworks with Genesis. Most frameworks depend upon specific classes on site elements, which means you’ll be writing a ton of functions to change or add to the existing Genesis class names. It’s much easier to simply build upon the existing Genesis CSS, or create your own CSS framework based upon it. If you like the column classes from unsemantic, copy over just that part (don’t enqueue everything else).

      In my base child theme I copied the table styling from Bootstrap, but I don’t enqueue the whole Bootstrap stylesheet.

  10. jezza101 says

    Just wanted to say thanks for your writing on this and the tips in the comments. I couldn’t have achieved my goal without this help. I was looking to make a dynamic AJAX type grid on a Genesis powered site and ended up using the technique here to get it going. A little jQuery magic allowed me to change the contents of the grid.

    Took some time to figure it all out but got there in the end. If anyone else is looking to do the same I have made a tutorial, linked via my name in the comment!

    Thanks Bill.