Troubleshooting & How-Tos 📡 🔍 Programming

Using the loop_end Hook With Multiple WordPress Loops

I recently added some custom navigation to the tag and category pages on my blog. I could have tweaked the tag template on the theme I’m using, or I could have created a child theme (which seems overkill for something this small). Since I already use a functionality plugin for the custom code I use on this site, it made more sense to add it there using WordPress hooks. That also means the next time I switch themes the new links will still be there.

The Problem

The clear approach was to use the loop_end hook, which runs when WordPress (or ClassicPress) finishes the Loop with all the posts for the current page. And it worked! Sort of. It added the link twice, once at the top of the page and once where it was supposed to.

It turned out one of the other plugins I use runs through the Loop when building preview cards, then rewinds it so the theme templates can display posts normally.

OK, how do I tell my custom function to only run on one iteration of the Loop, and which one?

A Janky Solution: Global Variables

After searching the WordPress documentation, and searching the web, and not finding anything remotely useful for combining loop_end and rewind_posts() (never mind any suggestion for best practices), I decided to go with an old standby: Set a global variable between Loops, and check that variable in my function.

Where to set the global variable? In this case, the first Loop runs in the HTML head, so I used the wp_body_open hook to change the variable state.

// Hack to only run my loop_end function when the Loop is running in the page body,
// since it gets run early when building the <head>
global $exampleprefix_building_page;
$exampleprefix_building_page = false;
function exampleprefix_set_building_page() {
	$GLOBALS['exampleprefix_building_page'] = true;
}
add_action( 'wp_body_open', 'exampleprefix_set_building_page' );

Then I checked whether $GLOBALS['exampleprefix_building_page'] was true or not in the function I attached to loop_end.

A Better Solution: did_action()

As it turns out, there’s a better way. True to Cunningham’s Law (the best way to get the right answer is not to post the question, but to post the wrong answer), the first reply to this article suggested that I use the did_action() method instead of messing around with a global variable. (Thanks, @gmazzap@phpc.social!)

So now I’m checking for did_action('wp_body_open') (If you have multiple Loops running in the page body, you’ll need to look for another dividing point) and I don’t need to mess around with globals after all!

Now, even though loop_end still gets called twice, my function only adds those links where it’s supposed to. And I won’t have to change it if I remove the plugin that’s running the extra Loop, or change themes (unless the new theme adds more passes through the Loop), or even switch between ClassicPress and WordPress.

Sample code

// Add link at the end of tag/category archives.
add_action( 'loop_end', 'exampleprefix_tag_fullsite' );
function exampleprefix_tag_fullsite( $query ) {
    if ( did_action('wp_body_open') && $query->is_main_query() && ( $query->is_tag() || $query->is_category() ) ) {
		$obj = $query->get_queried_object();
    	$slug = $obj->slug;
        echo '<p><a rel="nofollow" href="https://example.com/tag/' . $slug . '">All pages site-wide with this tag</a></p>';
    }
}