Split handling of the WordPress loop

If you’ve ever built a custom WordPress theme, you’ll have no doubt worked with The Loop. If you aren’t quite sure what I’m talking about, the WordPress loop is a central part of all templates, as it allows the developer to display posts from a WordPress query object in a safe and future-proof way.

Often, looping through all posts in the query and handling them all in the same fashion is enough. This is typically how we build an archive template. Every now and then, however, the need for variation within the loop arises. I’ve seen a number of ways to handle this and they almost always have a common pattern: define a count variable, increment that variable by one on each iteration of the loop, then render different markup depending on the value of the count.

Another approach might involve accessing the global $wp_query; variable directly to create multiple loops, but that can open up the potential for your code to break down the track, should WordPress change it’s internals in way that doesn’t work with your code.

I thought I might share a slightly different approach to ‘splitting’ the loop without too much breaking of the mould…

The desired markup

Just so we know what we are aiming for, let’s define the markup we are aiming for…

<!-- The first article -->
<article class="Article">
    <header class="Article__header">
        <h1><a href="http://example.com/article/1">Post 1</a></h1>
    </header>
    <div class="Article__content">
        Lorem ipsum dolor sit amet, mel iriure prompta et…
    </div>
</article>

<!-- Remaining 9 articles displayed as clickable titles only -->
<div class="SubsequentPosts">
    <article class="Article">
        <header class="Article__header">
            <h1><a href="http://example.com/article/2">Post 2</a></h1>
        </header>
    </article>
    <article class="Article">
        <header class="Article__header">
            <h1><a href="http://example.com/article/3">Post 3</a></h1>
        </header>
    </article>
    
    <!-- … articles 4 through to 9 … -->
    
    <article class="Article">
        <header class="Article__header">
            <h1><a href="http://example.com/10">Post 10</a></h1>
        </header>
    </article>
</div>

The basic idea here is that we end up with one article at the top of the page in one format, then all remaining articles in the query appear in a different format, wrapped in a container for alternate styling.

The technique I typically see

The most common approach I see to achieve this involves setting a $count variable, incrementing that on each loop iteration, then handling the variation in the code by checking the count. Here is an example of how that might look;

<?php if ( have_posts() ): ?>
   <?php $count = 0; while ( have_posts() ): the_post(); ?>

      <?php if ( $count === 1 ): ?>
            <div class="SubsequentPosts">
      <?php endif; ?>

      <article class="Article">
            <header class="Article__header">
                <h1><a href="<?php the_permalink() ?>"><?php the_title() ?></a></h1>
            </header>
            <?php if ( $count === 0 ): ?>
                <div class="Article__content">
                    <?php the_content() ?>
                </div>
            <?php endif; ?>
      </article>

      <?php if ( $count === 9 ): ?>
            <div class="SubsequentPosts">
      <?php endif; ?>

   <?php $count++; endwhile; ?>

<?php else : ?>
    <!-- 'No Posts' fallback here... -->
<?php endif; ?>

There isn’t anything wrong taking this approach and it will allow you to get where you need to be. For me, however, the existence of the count conditionals adds unnecessary complexity and takes a little effort to piece together in my mind; it’s also easy to misinterpret, resulting in potential issues that you need to spend more time debugging later.

My preferred approach to split handling the WordPress Loop

I prefer to split the handling of the loop into two – one loop to handle the first post, then a second to handle all subsequent posts. Doing this leaves me with code that is much easier for me to come back to later and just understand without thinking too hard about it.

One way to split the query into two is to access the posts array directly and call the necessary WordPress template tags using the WP_Post object for context, but there’s a simpler way – we can simply change the condition in the while loop. Simply put, we set the while condition to only run while we are on the first post in the query object. After that is done, we can kick off a second while loop using have_posts() as the loop’s condition, and we’ll end up right where we need to be.

Now, one other requirement for this approach is to check the current_post property within the global $wp_query object – this will let us know where in the loop we currently are. Before WordPress starts the loop, the value of the property will be -1, so we need to create a function that adds 1 to this, resulting in the correct loop index. Here is a look at the PHP code without the markup, for clarity;

<?php
/**
 * Returns the index of the current loop iteration.
 *
 * @return int
 */
function pdk_get_current_loop_index()
{
   global $wp_query;
   return $wp_query->current_post + 1;
}
?>

<?php if ( have_posts() ): ?>

   <?php while ( pdk_get_current_loop_index() === 0 ): the_post(); ?>
        <!-- First post markup here -->
   <?php endwhile; ?>

   <?php if ( have_posts() ): ?>
        <!-- Subsequent posts wrapping markup starts here -->
      <?php while ( have_posts() ): the_post(); ?>
            <!-- Markup for subsequent posts here -->
      <?php endwhile; ?>
        <!-- Subsequent posts wrapping markup ends here -->
   <?php endif; ?>

<?php else : ?>
    <!-- 'No Posts' fallback here... -->
<?php endif; ?>

A note on functions and organisation

The best place for the pdk_get_current_loop_index() function we’ve created is probably your functions.php file, or even a core functionality plugin. This makes the function accessible from all templates, so you can use it in other loops as well.

The full example

Now that we’ve stepped through the building blocks, let’s take a look at the full example (let us also pretend the pds_get_current_loop_index() function is now in our functions.php file);

<?php if ( have_posts() ): ?>

   <?php while ( pdk_get_current_loop_index() === 0 ): the_post(); ?>
        <article class="Article">
            <header class="Article__header">
                <h1><a href="<?php the_permalink() ?>"><?php the_title() ?></a></h1>
            </header>
            <div class="Article__content">
                <?php the_content() ?>
            </div>
        </article>
   <?php endwhile; ?>

   <?php if ( have_posts() ): ?>
        <div class="SubsequentPosts">
         <?php while ( have_posts() ): the_post(); ?>
                <article class="Article">
                    <header class="Article__header">
                        <h1><a href="<?php the_permalink() ?>"><?php the_title() ?></a></h1>
                    </header>
                </article>
         <?php endwhile; ?>
        </div>
   <?php endif; ?>

<?php else : ?>
    <!-- 'No Posts' fallback here... -->
<?php endif; ?>

To me, this code is much more readable at a glance and involves less brain power to interpret. Depending on how you work, you might take this further and break the markup out into template partials, but that is beyond the scope of this article.

Conclusion

I think it’s fair to say that, as WordPress developers, it’s easy to get stuck in the same patterns. Patterns are valuable, as they allow us to work quickly and confidently, not having to solve the same problem over and over. But, as this example shows, thinking laterally – in this case, by changing the expected have_posts() conditional – can have some really nice results.

Need a website? Let's talk.

From website design & SEO through to custom WordPress plugin development. I transform ideas into dynamic, engaging, and high-performing solutions.
Subscribe to get the latest insights & updates in the world of web and how it impacts your business and website.
© 2024 Phil Kurth  |  All rights reserved.