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:
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:
- The main category ID
- An array of posts belonging to the main category’s subcategories (the
$posts
global array).
We need:
- The subcategories’ IDs
- 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:
- 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 eachget_posts()
, we can display what is needed.
The main drawback in this method is the need to access the DB (albeit usingget_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. - 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 theget_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_id
s – 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_id
s 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_id
s, 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:
name | term_id | post_ids |
---|---|---|
January | 238 | 451755 , 450433 , 452135 , 451991 , 451944 , 452169 |
February | 241 | 452295 , 452421 , 450619 , 452410 , 452402 |
March | 233 | 451702 , 448953 , 451685 , 445431 |
To that end, we’ll use the GROUP BY
MySql function to group together the term_id
s, 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_id
s.
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.
In Step 1: (Function.php)
main_cat_id = get_category_by_slug( ‘main-cat’ )->term_id;
Parse error: syntax error, unexpected ‘=’
Thanks for pointing that out. The issue is actually caused by the missing $ in front of $main_cat_id. I’ve fixed it above ?
Hi
Hope you are well.
I need help with this “5. Display the Post List Under Its Respective Subcategory”
I don’t have much knowledge about advanced php
Please Help me
Hi
Sure, I’ll try to help, but forst I have to understand where the problem is.
Have you copied the code that is written in that section, and pasted it in the
category-main-cat.ph
p file that we created in section 3? What happened then?