How to Display Posts on a Category Page, Divided Into Their Respective Subcategories

wordpress sort categories and subcategories

Say you have a category page on which you’re asked to display all the posts that belong to that category’s subcategories. So far no sweat – a category page displays by default all its descendant posts, whether they belong directly to that category or any of its subcategories. But here’s the twist: say you have to display them in such a way that the subcategory names are displayed (challenge no. 1), and under each subcategory’s name you have to display all its posts (challenge no. 2).

Let me give an example of when you’d want to achieve such a thing: For instance, if you had a main category named “State-Wide Schools” which had subcategories, each of which was a city, and the posts were the schools in that city (each post detailing the properties of said school). Another case could be that the main category is a year, the subcategories are the months in that year, and the posts are events that happened that month.

Here is an illustration of the required category-page structure:

posts in categories and subcategories

Category page structure based on infographic vector created by freepik

This post will take us step by step to achieving that goal.

Summary of How Categories Work in WordPress

Before we start, let’s quickly review how categories work in WordPress. As previously said, the default behavior of a category page is to display a list of all the posts in that particular category. The template file within the theme that’s in charge of rendering the category’s display is category.php.

If we want a certain category to be displayed differently, we target it with its own template file: the file’s name will start with the word category, followed by a hyphen and then by either the category’s slug or its ID. This filename format enables the WordPress engine to recognize it as a specific category template. For example, category-13.php will display the posts in the category whose ID is 13, and the category-mycat.php would display the posts in the category whose slug is mycat.

In the case I’ll describe in this post, I decided to use the slug since the site admin has control over it and therefore it can be retained across different sites. This is as opposed to an ID which is automatically generated and therefore we’d have to change the template file’s name on each site to accommodate the different category IDs. That, of course, is not practical.

While this template file takes care of displaying the posts on the page, the other important issue is getting the data – as we noted before, by default WordPress doesn’t provide the data about subcategories in a category page.

There are a few different ways to retrieve the subcategories and the posts belonging to them—we can use the common WordPress functions, or we can construct a custom $wpdb query. This case required me to use a custom query, for reasons I’ll explain now.

Why a Custom $Wpdb Query?

I must admit that I can’t recall ever having to write a custom SQL query in WP—I’ve always managed to do what was needed and get the required data by using built-in WordPress queries. In this case, however, I believe that there would be major performance issues had we chosen other ways to retrieve the data. Let me walk you through the thought process that led me to succumb to writing SQL with my bare fingers.

We’ll start by identifying what data we do have and what we have to get.

We have:

  1. The main category ID
  2. An array of posts belonging to the main category’s subcategories (the $posts global array).

We need:

  1. The subcategories’ IDs
  2. A list that connects each of those subcategory IDs to the posts belonging to it

Getting The Subcategories’ IDs

This is actually relatively easy since we can use the get_terms() function, passing it the parent category’s ID in the parent field. You can see a usage example in this WPSE answer.

Getting the Subcategory That Each Post Belongs To

This, however, is the challenging part since the $post object doesn’t have any field connecting the post to a category ID. Therefore in order to retrieve the relationship, we can go in either of these two paths:

  1. Loop through all the subcategories retrieved in the previous section, and for each of them run the get_posts() function, passing it the subcategory ID.
    Then, using the data from each get_posts(), we can display what is needed.
    The main drawback in this method is the need to access the DB (albeit using get_posts()), as many times as there are subcategories – not very performant. The second drawback is that this method makes the $posts array redundant, and that’s a waste of resources.
  2. The alternative is to go the other way around and get the subcategories’ IDs using the post IDs. This means looping through the $posts array, and for each post, getting its category using the get_categories() function. This, in and of itself is highly unperformant, because assuming we have at least a few dozen posts it means accessing the DB multiple times. On top of that, we’d have to save the data in an array and then loop through it to display the subcategories and their posts.

Since those are the main ways of getting the info, and each of them has performance issues, not to mention producing verbose code in order to work, I decided to use the more concise and more performant custom SQL query.

Now that we understand the grand scheme of things, we can go into the details of implementing the code.

Diving Into the Code

We’ll start by (1) retrieving all of the category’s posts, then (2) we’ll create a function that gets a list of the subcategories and the post IDs that belong to each of those subcategories. After we have this data, (3) we’ll call that function from the template-file that we’ll create for this specific category, then we’ll (4) parse the post’s info by subcategory, and (5) use it to build the page’s HTML.

1. Let’s Get All of the Category Posts

We’ll start with the easy step: getting all of the category’s posts without paging. To do that, we’ll attach a function to the pre_get_posts hook, and pass -1 as the number of posts to fetch when it gets the posts for our given category:

$main_cat_id = 0;

function init_variables() {
	if ( term_exists( 'main-cat', 'category' ) ) {
		main_cat_id = get_category_by_slug( 'main-cat' )->term_id;
	}
}

function hook_into_wordpress() {
	add_action( 'pre_get_posts', 'pre_get_posts' );
}
public function pre_get_posts( $query ) {
	if ( ! is_admin() && $query->is_main_query() ) {
		/*** Check that the main category ID exists on the site, and then check that we're on that category's page ***/
		if ( term_exists( main_cat_id, 'category' ) && $query->is_category( $main_cat_id ) ) {
			$query->set( 'posts_per_page', - 1 );
		}
	}
}

As you may have noticed, we apply this code to a specific category with a specific slug, main-cat. While this method is not ideal in that if we ever want to extend this behavior to other categories we’ll have to change the code, we’re going with this simplified example; enabling more flexibility would take us beyond the scope of this article.

2. Let’s Get the SubCategories and The the Post IDs Assigned to Them

In this section we’ll create a function that gets a list of the subcategories and the post IDs that belong to each subcategory.
This challenge is a double one: first of all, when we’re in the category page we have no data about subcategories, neither from the category side (does it have subcategories, and if so, who they are) nor from the side of the post (the post objects, stored in the $posts global variable, have no property that ascribes them to any category). If we want any information about subcategories, even the most trivial such as their names, we have to retrieve it from the database.

Therefore, our only way of getting information about subdirectories is to retrieve from the DB a list of the main category’s subcategories, and for each said subcategory – a list of comma-separated post IDs belonging to it.

We’ll conquer the first challenge using two tables: wp_term_taxonomy and wp_terms .
The wp_term_taxonomy table stores information about the relationships between the various categories, using 2 main columns: parent and term_id. Therefore this is the table that we’ll query to find out what the main category’s subcategories are, and we’ll do so by sending it the main category ID as the parent, and select all the term_ids – which are the subcategory IDs – whose parent column is the main category ID.
The wp_terms table holds the information about each category – its name, slug, etc. Hence, using the term_ids that we retrieved from the wp_term_taxonomy table, we’ll select the subcategories’ names from the
wp_terms table.

The second challenge calls for using the wp_term_relationships table. This table has an object_id column and a term_taxonomy_id column, and each row represents a connection between an object (a post, or media file, or any such single entity in WordPress) and a term (could be a category, a tag, or any kind of group entity) assigned to it. Therefore, we could theoretically query that table sending the subcategory IDs as term_ids, and get all the posts belonging to that subcategory. However, that would give us a dataset of rows for each subcategory, which is harder to manipulate. Therefore we’ll query it in such a way that we get one row per subcategory, and that row will have a column with a comma-separated list of post IDs.

An example of a data resultset:

nameterm_idpost_ids
January238451755 , 450433 , 452135 , 451991 , 451944 , 452169
February241452295 , 452421 , 450619 , 452410 , 452402
March233451702 , 448953 , 451685 , 445431

To that end, we’ll use the GROUP BY MySql function to group together the term_ids, and we’ll also use the GROUP_CONCAT function which will create a comma (or any other separator of your choice) separated list of the object_ids.

So this is the query that gets all the information we need – (1) the main category’s subcategory IDs, (2) their names, and (3) a list of post ID’s belonging to each category. We’ll put in a function called getChildCategoriesAndPosts :

 $childCategories = $wpdb->get_results( "SELECT {$wpdb->prefix}terms.name, {$wpdb->prefix}terms.term_id, GROUP_CONCAT({$wpdb->prefix}term_relationships.object_id SEPARATOR ' , ') AS post_ids 
FROM {$wpdb->prefix}term_taxonomy
INNER JOIN {$wpdb->prefix}term_relationships on {$wpdb->prefix}term_relationships.term_taxonomy_id = {$wpdb->prefix}term_taxonomy.term_id
INNER JOIN {$wpdb->prefix}terms on {$wpdb->prefix}terms.term_id = {$wpdb->prefix}term_taxonomy.term_id
WHERE wp_term_taxonomy.parent=$cat
GROUP BY {$wpdb->prefix}terms.term_id
ORDER BY {$wpdb->prefix}terms.name;" );

To easily iterate over the list of post IDs, we’ll convert the list to an array:

foreach ( $childCategories as $child_category ) {
      $child_category->post_ids = explode( ',', $child_category->post_ids );
}

If you’re asking yourself if it is absolutely necessary to write a custom SQL query, if this mission couldn’t be accomplished using WordPress functions, stay tuned – I’ll address that towards the end of the post.

3. Calling the function from a template file

In this section, we’ll call that function from the template-file that we’ll create for this specific category.

Let’s create a PHP file using the category’s slug (for example, if the category’s slug is main-cat, then our file’s name will be category-main-cat.php). This file will be used by WordPress when the user browses to this category page (see Template Hierarchy examples of Category or the Template Hierarchy visual overview ).

In this file, we’ll create a <ul> element whose list-items will be the subcategories.

<ul class="subcategory-list">
   <?php
   global $cat, $post;

   // get an array of category IDs, names, and a list of each category's posts
   $subcategoriesList = getChildCategoriesAndPosts( $cat );

   /* Loop over the subcategories, and for each subcategory print its posts' title and link
   * The posts are retrieved from wp_query, by post ID
   */
   foreach ( $subcategoriesList as $subcategory ) {
      ?>
      <!-- We'll fill this in section 5 -->
   <?php } ?>
</ul> 

The next step will be to loop through the dataset of subcategories we got from getChildCategoriesAndPosts, and display its posts’ data. But before we can do that, we have to create a function that extracts the post info from the $posts array.

4. Let’s Extract the Post Info from the $posts Array

Each iteration over a subcategory will have a nested iteration over the post IDs belonging to it. We’ll use that post ID to get the relevant info and display it under the current subcategory name.
How will we display each post’s info according to the posts ID? As we mentioned at the beginning of the post, the Category page’s default behavior is to retrieve the category’s posts and store them in a global array called $posts. So we’ll write a function which is passed a post ID, and it will extract that post’s info from the $posts array using the array_filter() method. Another small method we’ll use is array_values() – because since array_filter returns an array item but not necessarily with the 0 index, we have to call array_values whose job it is to return all the values from the array and index the array from 0. We’ll create this function in the Categories class and name it getCurrentPostFromCategoryList. Then we’ll call it from our main category’s template file, category-main-cat.php.

Here is the function:

function getCurrentPostFromCategoryList( $post_id ) {

    global $posts;
    $ret = null;
    $curr_post = array_filter( $posts, function ( $obj ) use ( $post_id ) {
        return $obj->ID == $post_id;
    } );
    $curr_post = array_values( $curr_post );
    if ( count( $curr_post ) > 0 ) {
        $ret = $curr_post[0];
    }

    return $ret;
}

And now we’re ready to use it.

5. Display the Post List Under Its Respective Subcategory

Going back to our category-main-cat.php, we’ll create an <li> element, and in it display the respective post info: the post title as a link to the actual post. We’ll do so by using a foreach loop to iterate over the array of IDs and calling the aforementioned getCurrentPostFromCategoryList function.

<li id="cat-<?php echo $subcategory->term_id; ?>" class="subcategory-category-wrapper closed">
      <h2 class="subcategory-category-title"><?php echo $subcategory->name; ?></h2>
      <ul class="post-list">
      <?php

      // Each subcategory has an array of post IDs. loop through them and retrieve the posts and print their title and link
      foreach ( $subcategory->post_ids as $post_id ) {
            // assign the current post to the global $post, so the template can use the loop's functions
            $post = getCurrentPostFromCategoryList( $post_id ); ?>
            <?php the_title( sprintf( '<li class="entry-title post-name"><a href="%s" rel="bookmark">', esc_url( get_permalink() ) ), '</a></li>' ); ?>
      <?php } ?>
      </ul>
</li><!-- #post-<?php the_ID(); ?> -->

And so we got a list of posts assigned to their parent subcategory, with only one query to the DB.


Add a Comment

Your email address will not be published. Required fields are marked *