Counterpoint - Hacking the WordPress Loop

Early on in every web developer's career, there comes a time when the Google teat runneth dry. All of a sudden, your good, clean, best-practiced code is in danger of mutating into cascades of if-statements and ugly hacks, just like your own brain.

Stop. Backspace that sentence and a half you've typed into the search bar and face the facts, man: Google has failed you. It's time to brandish your machete and start hacking.

Meet Counterpoint

Counterpoint is the project that's taken up the majority of my life for the past 3 months. It is a full-scale WordPress theme (which handles the blog's visual presentation), and is currently undergoing a review process before it is accepted into the WordPress Theme Directory, so anyone creating a new WordPress blog can use it as their theme, free of charge.

The development process was pretty intense, and I learned a ridiculous amount along the way. All in all, the code spans 39 files totaling 11 megabytes. And that doesn't even count all the debugged and rewritten code, which is where most of my time was spent.

It's not perfect. There are still plenty of things about it, visual or otherwise, which give me the heebie-jeebies. But it's really become something of a companion: as Counterpoint evolves, new coding challenges arise, pushing me to further my skills. As my skills develop, I test them out on Counterpoint, and the cycle continues.

During my journey I've been helped by an army of codices, blog posts, and even real-life humans. This post is an opportunity to give back, in whatever small way, so that when others confront similar issues, my solution is available as a reference.

The Goal

OKAY. At the top of my theme's blog index, I want to display the most recent sticky post (for the non-bloggers out there, you can mark a post "sticky" to make it appear at the top of your blog). The sticky spans the entire width of the content area. After that, I want all other posts to display normally, with a user-defined number of posts per page.

So, if the user sets their posts-per-page to 6, there will be a single sticky and six more posts on the front page---for a total of 7---and 6 normal posts on each subsequent page.

The reason for this is that I chose to display in 2 columns, and I don't want a big open space at the bottom-right side of the page. Because stickies stretch the entire width of both columns, taking up the space of two regular posts, it is necessary to ensure that the front page gets served an even number of posts following the sticky.

Image of the big ugly gap at the bottom of the homepage

Otherwise this happens.

The Problem

Of course, I wouldn't be writing this post if it was trivial.

I'm not going to get too deep into the nitty-gritty on this post, but if you're interested in a more detailed explanation of the WordPress loop, there are plenty of good resources out there. For now, what you need to know is that "the loop" is what grabs all your blog posts via a query, which can be given different arguments to retrieve the exact posts you need.

The problem lies in the way WordPress handles sticky posts. If the sticky post is already supposed to appear on the front page, the usual behavior is to simply move it to the top. However, if the sticky post doesn't naturally appear on the front page, it will be added to the front page IN ADDITION TO appearing in its natural position. Let me rephrase that: sticky posts will appear either once OR twice depending on its position in the query.

It gets even more complicated. When a sticky is drawn from outside the front page, it doesn't count towards the posts-per-page limit. This means we'll end up displaying a different number of posts depending on where the sticky came from. Theoretically, even with posts-per-page set at 6, the front page could have any number of additional posts, depending on the number of stickies that were prepended to the query.

It's an interesting solution to avoiding duplicate material on the front page, but as you can see it does lead to some frustrating design issues:

We can see that the normal loop is not going to help us here. No matter our settings, there is going to be a gap somewhere, and as a perfectionist, I cannot let that happen.

The Solution

The solution is two loops. The first loop displays the most recent sticky. The second displays the rest. However, if the sticky is from the front page, we get that silly duplication like this:

Example of a duplicated sticky post

If it's such a problem, why don't I just skip that post in the second loop, thereby removing the duplicate? Unfortunately, to get a theme accepted into the WordPress Theme Archive, the natural behavior of duplicating a sticky in its normal, non-front-page position is REQUIRED. We'll have to be more clever than that.

Let's get started.

/* Loop #1 - for stickies
========================== */

// get an array of stickies
$stickies = get_option('sticky_posts');
// get the first one, or set it to false if none
$first_sticky = $stickies ? end($stickies) : false;
// get the posts_per_page variable (set by user)
$ppp = get_option('posts_per_page');


//  if a sticky exists and it's the front page...
if ( $first_sticky && is_front_page() && !is_paged() ) {

    // query the most recent sticky post
    $most_recent_sticky_post = new WP_Query( 'p=' . $first_sticky );

    // and display it
    while ($most_recent_sticky_post->have_posts()) : $most_recent_sticky_post->the_post();

        // code to display the post

    endwhile;

}
wp_reset_postdata();  

This is easy enough. Get the first sticky, query it, and display it. Then use wp_reset_postdata() to make sure subsequent queries are unaffected.

Before moving on to the main query, we first need to determine where the sticky came from. Sadly, this was one of those instances where google was not much help. My sketchy solution was to do an extra query and search for the sticky in the first 'n' posts, where 'n' is the user-defined posts-per-page. The result is stored in the variable $front_page_sticky. It's convoluted, but to my knowledge, it is the only way to do what I want.

$front_page_sticky = false;

// only do this if there's a sticky in the first place
if ( $first_sticky ) {

    // so here's the junk query
    $junk_query = new WP_Query(array(
        // ignore normal sticky behavior
        'ignore_sticky_posts' => 1
    ));

    // loop through the posts in $junk_query
    foreach($junk_query->posts as $post_num=>$junk_post) {

        // if it comes across the $first_sticky within the first page of posts...
        if ($post_num < $ppp && $junk_post->ID === $first_sticky) {
            // set this to true, then quit
            $front_page_sticky = true;
            break;

        // if there's no match, leave $front_page_sticky false...
        } elseif ($post_num >= $ppp) {
            break;
        }
    }
    wp_reset_postdata();
}

Cool. Now we have all the variables we need to set up our query arguments (we'll store them in a variable called $cp_args). Because every page of our results runs this same code, it's vital to make sure that it's served a specialized query for each page. Based on our desired behavior, there are two situations which require us to alter the default query:

  1. if there is a front-page sticky
  2. if we're not on the front page

Since these are not mutually exclusive, we'll check each with separate if-statements, adding the appropriate arguments to $cp_args.

// ignore sticky behavior no matter what
$cp_args = array(
    'ignore_sticky_posts' => 1
);

// if there is a front-page sticky, skip it in the query
if ( $front_page_sticky ) {  
    $cp_args['post__not_in'] = array($first_sticky);
}

// if we're not on the front page, set an offset so we don't get the same posts as the front page
if ( is_paged() ) {  
    $cp_args['offset'] = ($wp_query->query_vars['paged'] - 1) * $ppp;
}

All done! Now we can run the query.

// now for the main query
$main_query = new WP_Query($cp_args);

while ($main_query->have_posts()) : $main_query->the_post();

    // code to display the post

endwhile;  
wp_reset_postdata();  

One Last Thing

Any time we're not on the main blog index (for searches and archive pages), stickies don't apply, so I just want to do a basic query. For that, I'm going to wrap everything in:

if ( is_home() ) {

    // all of the previous code

} else {

    // run the default query if is_home() returns false
    // (i.e. search and archive pages)
    while (have_posts()) : the_post();

        // code to display the post

    endwhile;
}

Reflection

The programming community is extremely open with its intellectual property. Pretty much any and all new technologies and techniques are piously shared with the rest of the community, such that anyone skilled in Google-fu will discover it's almost hard NOT to find a wholesale solution to your exact problem, right there on the front page.

It makes sense. There's always bigger and more interesting problems to solve down the line, and it's lost productivity every time we reinvent the wheel---intentionally or otherwise. This culture of openness allows us to live and die on the bleeding edge. It's no wonder why so many programmers are passionate about open source projects---it's not just philosophy, it's the only game in town. As new programmers cut their teeth, they stand not on the shoulders of giants, but on those multitudes of code-pushing peons who surfaced for just a moment to ask a poorly-phrased question on a public forum.


Music. Math. Computers. As I move on to the next big thing, I am more and more sure that each step forward in programming is also a step towards self-actualization. In the set of variations so complete and sophisticated as one's life, the theme is often difficult to discern. But we can pick up on common aesthetic motifs, and in doing so, sometimes we get a glimpse of the whole composition. Eventually the threads will all come together, like the Finale of Beethoven's 3rd Symphony, a sonic boom of unity and perfect counterpoint.