Categories


Archives


Recent Posts


Categories


Changing the WordPress Front Page Query

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

Last week I had one of those “quick hack that works for years” finally reach its half life, with the result being a few hours with a blank homepage.

Today’s article is going to be a pilgrimage through the various parts of WordPress I had to touch to fix this The Right way. “The Right Way” is, of course, a construct based on your values and what you’re trying to get out of a system. The actual right way is probably to use one of the many WordPress plugins for customizing your front page.

Reminder: The hack/bug was this code

<?php while(have_posts()) : the_post(); ?>
    <?php if(get_post_format() === 'link'){ continue; } ?>
    <?php if(in_category('Programming Quickies')){ continue; } ?>
    <?php get_template_part('layouts/posttitle'); ?>
    /* ... */
    <?php break; ?>
<?php endwhile; ?>

I wanted to exclude certain post types from the WordPress front page. I got the behavior I wanted by adding some guard clauses to the while(have_posts()... loop. This ended up being a problem when all the posts (of the 20 queried for) were of the types I wanted to omit.

The fix we’re chasing is having WordPress run its post query in such a way that these posts are excluded only on the front page.

Finally, a caveat. I don’t spend my day in the bowels of WordPress, so I’m likely using terminology that’s incorrect. Corrections are welcomed. Sometimes the only way you learn something is to blunder in and start swinging your arms around like a fool.

The Loop or a Custom WP_Query?

The first question to answer: Can I do what I want to do with a custom WP_Query object, or do I need to rely on The Loop™’s global query. Lacking other factors, my preference would be to create a new query object that does exactly what I need and doesn’t risk changing the global query in an unexpected way.

Unfortunately, there is another factor. I’m using a third party theme, and their front page template looks a bit like this

<?php while(have_posts()) : the_post(); ?>
    <?php get_template_part('layouts/posttitle'); ?>
    <!-- ... -->
    <section id="post-<?php the_ID(); ?>" <?php post_class("postcontents wrapper"); ?> itemprop="articleBody">
    <!-- ... -->
    <?php the_content(); ?>
    <!-- ... -->
    <?php get_template_part('layouts/postbottom'); ?>
    <?php get_template_part('layouts/authorinfo'); ?>
    <!-- ... -->
<?php endwhile; ?>

My (possibly incorrect?) understanding of WordPress functions like the_ID or the_content is they’re pulling data from the current post in The Loop™. There’s similar function calls deeper in each of the templates rendered by get_template_part.

I could rewrite all of this to use a custom WP_Query object, but that means rewriting a bunch of template files I don’t own. Each change to these files has the same risk as a change to the global query — so now my reasoning is it’s better to make a single change to the global query than multiple changes to multiple files that I don’t technically own.

Which Hook/Event

Next up is figuring out how to change the global query object. WordPress has an event/observer system that they call hooks. There are action hooks and filter hooks. Action hooks allow you to register a listener that will run some code when something happens. Filter hooks allow you to register a listener that will run some code when something happens, and this code will have a chance to modify some piece of data. Action hooks are further separated: Some action hooks will pass you variables by reference, with the intent being that your listener has a chance to modify the referenced variable. These are sometimes called “ref array” action hooks.

There is an action hook named pre_get_posts that WordPress invokes immediately prior to running the SQL queries for a WP_Query object. This action hook is a ref array action hook that gives you an opportunity to modify the passed in WP_Query object.

To listen for this pre_get_posts event, we’d start with code like this.

add_action('pre_get_posts', function($query) {
    // change query here
});

Just the Front Page

Since pre_get_posts will see all WP_Query queries, we’ll need to add some guard clauses to our listener function.

add_action('pre_get_posts', function($query) {
    if(!$query->is_main_query() || is_admin() || !is_front_page()) {
        return;
    }

    // modify query here
});

There’s three things we check for. First, WP_Query objects know if they’re “the main” query. This is what I’ve been calling the global query. The is_main_query() method will return true or false depending on whether this particular query instance is the main query or not.

Second — the is_admin function tells us whether we’re rending an admin page or looking at a front end content page.

Third — the is_front_page function tells us if we’re rendering the front page of the WordPress site, or rendering another page.

The is_admin calls seems redundant here (and it is), but it never hurts to be extra paranoid in a system where arbitrary plugins can change the behavior of your system in countless ways.

With these three checks in place, we’re ready to move on to modifying the query.

Every Category Save One

The WP_Query object is a class that offers us an API for fetching posts from the WordPress database. While posts live in WordPress’s wp_posts database table, it’s not always as simple as “one database row equals one post”. Additionally, some post data (categories, tags, etc.) is spread out across other database tables, and the WordPress database itself contains no foreign keys that map these relationships. WordPress’s PHP code is where there relationships are defined and these relationships have changed between versions of WordPress.

In other words — WP_Query objects end up being the source of truth for what “a post” is in WordPress.

The pre_get_posts event will send us a query object right before it starts making SQL queries. Our first task is to modify this query object so that posts in the Programming Quickies category are omitted from the query. If we look at the docs is seems like the category__not_in query param is what we’re looking for. However, now there’s two additional challenges.

  1. Every example in the docs shows us how to instantiate a query with parameters, but not modify a query
  2. The category__not_in parameter wants an id. Since IDs are just auto increment integers they could change if we ever rebuild this particular system

Poking a bit at the WP_Query class, the set method appears to solve our first issue. It will allow us to set a query parameter/var after a query’s been instantiated

#File: wp-includes/class-wp-query.php
/**
 * Set query variable.
 *
 * @since 1.5.0
 * @access public
 *
 * @param string $query_var Query variable key.
 * @param mixed  $value     Query variable value.
 */
public function set($query_var, $value) {
    $this->query_vars[$query_var] = $value;
}

The problem of not using a hard coded category id is a little more involved. The WP_Query object gives us the ability to query by a category’s slug (programming_quickies), which is a better identifier for our purposes. Unfortunately, it doesn’t give us the ability to query for NOT a particular slug. One option is to query for every slug in the system, create a comma separated string, and use that as the category_name parameter

$slugs = array_filter(array_map(function($item){
    if($item->slug === 'programming-quickies') {
        return null;
    }
    return $item->slug;
}, get_categories()));

$post_formats = get_post_format_slugs();
$query->set('category_name',implode(',',$slugs));

This works, and is the first solution I came up with, but grabbing every category in order to exclude one seems inefficient. It’s probably better to lookup the Programming Quickies database ID and use that in a category__not_in query.

The first WordPress function I found related to this was get_cat_ID — which returns the category id if you give it a category name.

This seemed promising — but it turns out that “name” here doesn’t mean the category slug — it means the actual name field. (despite category_name meaning the slug in a WP_Query object)

// this works
get_cat_ID( 'Programming Quickies' )

// this does not
//get_cat_ID( 'programming-quickies' )

Next I found get_category_by_slug which returns a WP_Term object whose term_id is the category ID. This means we can do something like this

$term = get_category_by_slug('programming-quickies');
if(!$term || !is_numeric($term->term_id)) {
    return;
}

$query->set('category__not_in', $term->term_id);

and the Programming Quickies category will be excluded from the query. The is_numeric portion of the guard clause is probably overkill — but not being familiar with an API and a long career can lead to this sort of paranoia.

Skipping Links

With programming quickies posts excluded, the final thing we’ll want to do is exclude posts whose format type is a link. This was a little trickier to implement and involved a WordPress taxonomy query. The Taxonomy system is how WordPress can group posts together in different ways. Adding a taxonomy query to the WP_Query ended up looking like this

$query->set('tax_query',
    [[
        'taxonomy' => 'post_format',
        'field' => 'slug',
        'terms' => array(
            'post-format-link',
        ),
        'operator' => 'NOT IN'
    ]]
);

I don’t have a lot to say about this one because I didn’t sit down and trace out exactly how taxonomy system works, but some subtitles are embedded in these two Stack Exchange questions.

Final Hook Observer

So, with all that said, the final hook observer ends up looking like this

add_action('pre_get_posts', function($query) {
    if(!$query->is_main_query() || is_admin() || !is_front_page()) {
        return;
    }
    $query->set('tax_query',
        [[
            'taxonomy' => 'post_format',
            'field' => 'slug',
            'terms' => array(
                'post-format-link',
            ),
            'operator' => 'NOT IN'
        ]]
    );
    $term = get_category_by_slug('programming-quickies');
    if(!$term || !is_numeric($term->term_id)) {
        return;
    }
    $query->set('category__not_in', $term->term_id);
});

Only time will tell if this ends up being a more stable solution than my original “five year hack” — but at least I understand the classic WordPress APIs a bit more.

Copyright © Alan Storm 1975 – 2021 All Rights Reserved

Originally Posted: 13th September 2021