Creating a Sidebar of Categories and Posts in WordPress

Recently, I was tasked with revamping the navigation on an internal WordPress site. Previously, the navigation was a hard-coded series of unordered lists and nested unordered lists on the homepage. Here’s an example of a single category and its subcontent:

  • Mammal
    • Dog
      • Border Collies
      • Pembroke Welsh Corgis
    • Fox

Mammal would be a top-level category in WordPress. A top-level category in this context is a collection of posts and subcategories.

Dog would a subcategory, which is a collection of posts.

Border Collie and Pembroke Welsh Corgi are two posts within the Dogs subcategory.

Fox is a post within the Mammal top-level category.

When you think about it, this data structure is the same tree navigation you see in Windows Explorer or your Mac’s Finder. You may open a folder (top-level category) that has a mix of files (posts) and other folders (subcategories) within.

I decided to convert this hard-coded navigation element into a fly-out sidebar generated with PHP, where each category would be a dropdown menu with posts listed within. The top-level dropdown menu (representing a top-level category) could have related posts or another dropdown menu (representing a related subcategory) within it. Here’s how that hypothetical list would look with the sample data:

wordpres-sidebar-example

The challenge for me was imagining how I would generate a multi-level sidebar with categories, subcategories, and posts. I needed to create a multi-level array, something that would allow me to iterate through it and build this sidebar while also giving me the ability to check whether a given item in that array was actually, for example, a post belonging to a top-level category or a subcategory with more related posts within it.

Combining categories and posts into a single array seemed difficult, given that a category query returned a WP_TERM object and a post query returned a WP_POST object. So I decided that the best way to go about doing this was to create a series of custom queries, targeting only the pieces of category or post queries that I actually wanted and needed.

To that end, I determined that at a minimum, I needed the following information about categories and posts to help generate the sidebar:

  • Title
  • Permalink

And then I added a third required item:

  • Sub

I added “Sub” as a way to represent a subarray. So for example, a top-level category would have a subarray of related posts and subcategories. And then a subcategory would have a subarray of related posts. For posts, the “Sub” item would be empty because it wouldn’t have any related subcategories or subposts. Tada!

The end result was a multi-level array with the following data structure, represented with JSON (note that this represents a single top-level category, Mammal):

[
    {
        "title": "Mammal",
        "permalink": "",
        "sub": [
            {
                "title": "Badger",
                "permalink": "http:\/\/localhost:8888\/archives\/174",
                "sub": []
            },
            {
                "title": "Cat",
                "permalink": "",
                "sub": [
                    {
                        "title": "Ragdoll",
                        "permalink": "http:\/\/localhost:8888\/?post_type=post&p=38"
                    },
                    {
                        "title": "Scottish Fold",
                        "permalink": "http:\/\/localhost:8888\/?post_type=post&p=36"
                    }
                ]
            },
            {
                "title": "Chipmunk",
                "permalink": "http:\/\/localhost:8888\/archives\/176",
                "sub": []
            },
            {
                "title": "Dog",
                "permalink": "",
                "sub": [
                    {
                        "title": "Border Collie",
                        "permalink": "http:\/\/localhost:8888\/?post_type=post&p=76"
                    },
                    {
                        "title": "Pembroke Welsh Corgi",
                        "permalink": "http:\/\/localhost:8888\/?post_type=post&p=90"
                    }
                ]
            },
            {
                "title": "Fox",
                "permalink": "http:\/\/localhost:8888\/archives\/88",
                "sub": []
            }
        ]
    }
]

Notice how posts within the top-level category “Mammals” (i.e., “Badger”, “Chipmunk”, and “Fox”) have an empty “sub” subarray. When iterating through my multi-level array of categories, subcategories, and posts, I was able to check for an empty subarray, confirm that that entry was a post and not a subcategory, and apply the appropriate HTML. Posts like “Ragdoll” or “Border Collie” which reside in subcategories do not have a subarray, because when I wrote the query for those posts, I did not even have to bother with that particular item; in this site, I knew that only posts would reside in a subcategory, not any additional categories.

So here’s how I built this multi-level array and then generated the sidebar.

First, I initialized my array, so that I could push my query results to it:

$post_data = array();

Then I created my initial query, where I gathered the top-level categories (like Mammal in the example above) and start populating the $post_data array in PHP:

<?php
// get the top-level categories
$cats = get_categories( array(
            'parent'  => 0
        ) );

// start populating the array
foreach ($cats as $cat) {
    $cat_array = array(
        'title' => $cat->name,
        'permalink' => '', // this will be blank for categories
        'sub' => array() // this is where we setup level 2 of this multi-level array
    );

    // call our function to locate posts in the current top-level category & populate the "sub" array
    findPosts($cat_array['sub'], $cat);

    // call our function to locate subcategories of the current top-level category & populate the "sub" array
    findSubCats($cat_array['sub'], $cat);

    // update the post_data array
    $post_data[] = $cat_array;
}
?>

Then I created two PHP functions: one to gather the posts that belong to primary categories, and one to gather all subcategories and their subsequent posts.

<?php
function findPosts(&$post_data, $cat) {
    $cat_id = $cat->term_id;

    $post_args = array(
        'category__in' => $cat_id,
        'posts_per_page' => -1,
        'orderby'=> 'name',
        'order' => 'ASC'
    );

    $plain_post_query = new WP_Query( $post_args );

    // get posts inside this category
    while ( $plain_post_query->have_posts() ) {
        $plain_post_query->the_post();
        $post_data[] = array(
            'title'         => get_the_title(),
            'permalink'     => get_the_permalink(),
            'sub'           => array()
        );
    }

    wp_reset_query();
}

function findSubCats(&$post_data, $cat) {

    $cat_id = $cat->term_id;

    $subcats_query = array(
        'child_of' => $cat_id
    );

    $subcats = get_categories( $subcats_query );

    wp_reset_query();

    // get the subcategories
    foreach($subcats as $subcat) {
        $array_item = array(
            'title'         => $subcat->name,
            'permalink'     => '',
            'sub'           => array()
        );

        $subcat_id = $subcat->term_id;

        $subpost_query = array(
            'category__in' => $subcat_id,
            'posts_per_page' => -1
        );

        $subposts = get_posts( $subpost_query );
        
        // get posts inside this subcategory
        foreach($subposts as $subpost){
            $subpost_id = $subpost->ID;
            $subpost_title = $subpost->post_title;
            $subpost_link = get_post_permalink($subpost_id);

            array_push($array_item['sub'], array(
                'title' => $subpost_title,
                'permalink' => $subpost_link
            ));
        }

        $post_data[] = $array_item;
        wp_reset_query();
    }
}
?>

Before I move on, here are some quick notes about the functions above:

  • In findPosts(), the while loop includes a “sub” subarray for each post found. In the context of this site, posts would never had subcontent. However, I still needed a blank subarray in order to compare it to a populated subarray for a subcategory. So for example, if you scroll back up to the sample array I posted earlier, you can see that items like “Badger” (a post) are on the same level as “Dog” (a subcategory). A quick and easy way for me to tell these two items apart when I iterate through the multi-level array is to see if these items have a populated “sub” subarray. To that end, I would find that Badger’s subarray is empty (and determine it’s a post!) and see that Dog’s subarray is populated (and determine it’s a subcategory with posts inside of it!).
  • In findSubCats(), I also queried for posts within subcategories, but I left the “sub” item out of $subpost_query. This is because I knew that the site had no additional subcontent (including tertiary categories or posts) nested beyond this point.
  • Notice how the parameters for each function above have &$post_data. The ampersand is necessary to pass the array by reference so that I can update the array.

The next step involved sorting. Sorting only posts or only categories in ascending order by title is simple enough, but the content in the sample “Mammal” category is a mix of posts and subcategories. In order to sort the posts and subcategories in alphabetical order, I had to rely on usort().

<?php
function cmp($a, $b) {
    return strcasecmp($a['title'], $b['title']);
}

usort($post_data, 'cmp');

for($sub_i = 0; $sub_i < count($post_data); $sub_i++) {
    $subs = &$post_data[$sub_i];
    usort($subs['sub'], 'cmp');

    for($page_i = 0; $page_i < count($subs['sub']); $page_i++) {
        $pages = &$subs['sub'][$page_i];
        usort($pages['sub'], 'cmp');
    }
}
?>

The second for loop is actually sorting the posts within the subcategories (posts like “Border Collie” and “Pembroke Welsh Corgi” in the sample data). This is not really necessary because in my query for those particular posts, I already included arguments 'orderby' => 'name' and 'order' => 'ASC'.

Finally, it’s time to iterate through this multi-level array to produce the sidebar:

<?php
$menu_dropdown_index = 1;
$submenu_dropdown_index = 500; 

foreach ($post_data as $d) {
    // a top-level category will be a dropdown menu
    echo "<li> <a data-toggle='collapse' class='menu-primary collapsed' href='#menu-dropdown-" . $menu_dropdown_index . "'><div style='float:left;'>" . $d['title'] . "</div> <span class='glyphicon glyphicon-menu-right' aria-hidden='true'></span> <span class='glyphicon glyphicon-menu-down' aria-hidden='true'></span></a>";

    echo "<div id='menu-dropdown-" . $menu_dropdown_index . "' class='first-list collapse'><ul>"; 

    foreach ($d['sub'] as $p) {
        // a post belonging to a top-level category will be a link
        if( count($p['sub']) == 0){
            echo "<li><a href='" . $p['permalink'] . "'>" . $p['title'] . "</a></li>";
        }
        // a subcategory within the top-level category will be a dropdown menu
        else {
            echo "<li> <a data-toggle='collapse' class='menu-secondary collapsed' href='#menu-dropdown-" . $submenu_dropdown_index  . "'><div style='float:left;'>" . $p['title'] . "</div> <span class='glyphicon glyphicon-menu-right' aria-hidden='true'></span> <span class='glyphicon glyphicon-menu-down' aria-hidden='true'></span></a>";

            echo "<div id='menu-dropdown-" . $submenu_dropdown_index . "' class='second-list collapse'><ul>";

            foreach ($p['sub'] as $sub) {
                // a post within a subcategory will be a link
                echo "<li><a href='" . $sub['permalink'] . "'>" . $sub['title'] . "</a></li>";
            }

            echo "</ul></div></li>";
        }

        $submenu_dropdown_index++;
    }
    echo "</ul></div>";

    $menu_dropdown_index++;
    echo "</li>";
}
?>

VoilĂ ! You have a sidebar for your WordPress site, generated with PHP!