31. January 2007 · Comments Off on Hierarchical Navigation in WordPress 2.1 · Categories: WordPress · Tags: , , , , , , ,

This is a long post about how I hacked a hierarchical navigation system into WordPress 2.1. If you’re just here for the code, head to the end.

The Vision
I wanted to convert a website that I help maintain from flat files to WordPress. This is a simple website, no dynamic content, just lots and lots of pages arranged in a hierarchical format. This maps very well to WordPress’ categories, so it shouldn’t be too hard, right? Ha.

The sidebar should show a list of all top-level pages, which we’re calling categories in WordPress terminology. Each of these categories will need a post or WordPress Page (which I’m going to capitalize here to differentiate it from a “page” which could be a post or Page) that serves as “what the user sees when they click on this.” So if I have three main sections of the website, each of those has some sort of title page that describes the section and might list the children. Typically WordPress category pages just show the most recent posts in that category, but we can easily change that if we decided to use them (I didn’t).

If we’ve stumbled into a child category, the navigation should show a breadcrumb trail from the correct parent category to the current child (there might be a few parent/child categories in between). Also, if the category we’re in has any child categories, those should show up. Finally, the navigation should show all the other posts in the current category.

I’m envisioning a simple, hierarchical navigation where you can see all the parents, how you got to your current category, and all the other pages and children in that category. The guts should be dynamic enough that if a person creates a new category (including header page) and puts a post in that category, it should all just magically appear.

You can see the flat-file implementation of this navigation system at the Center for Effective Discipline. This is the site I want to move to WordPress.

Some Details

My initial thought was to just use categories and so when you click on a navigation page, it takes you to the appropriate category page. The only problem with that is that, for this site, the header pages generally echo the navigation in that they list their child pages. We would have to use full permalinks here because WordPress forces you to put category pages in a different hierarchy then the pages. For example, if you click on the category A’s page, it takes you to /wp/category/a but all of the pages in category A are stored in /wp/a. I want the post authors to just be able to write “new-page-slug/” as the reference, and not the whole permalink. So categories as header pages are out.

Instead we have to use Pages. Unfortunately, Pages aren’t associated with Categories inside WordPress. This can be solved by Yellow Swordfish’s Page Category Organizer plugin, which is what I used. What we have to do is basically map Pages to categories, and have them use the exact same names and (most importantly) slugs as the category they’re emulating. This allows us to look at the category we’re in, find it’s slug and then know that the page we need will be at the same slug, just without the “category” subdirectory.

Logically, we’re not too bad. We just need to generate a list of parent categories, replace these with the appropriate page instead, then list the needed child pages (if any) and finally print out all the pages (and child categories, if there are any) in this category. Easy!

Smokey, my friend, you are entering a world of pain.
The title of this post explicitly says WordPress 2.1 for a reason. The main reason is that with this release the WP team deprecated the old ways of listing categories, and now want you to us wp_list_categories(). No problem, except that this new version (wait for it) doesn’t support the echo parameter. This translates to: we can’t manipulate the output. So we’re hosed, except that wp_dropdown_categories() does support echo. This translates to a lot more junk for us to have to parse out, but it gives us the starting point we need. It only goes downhill from here.

Overview of solution
The code I ended up writing is a over 100 lines long, but here’s what it does in a nutshell (and in order):

  1. Find out what category we’re in. This is more complicated than normal, because we might be on a Page which is category-less in WordPress. This is where the plugin listed above comes in.
  2. If we’re in a child category, create an array that holds the chain of parents up to the root parent category. We’ll need to walk this backwards when we print out those top level parents, so that under the correct one we see it’s child, and then that child’s child and so forth down to our current category. This is the “breadcrumb navigation” part.
  3. Get a list of all the Pages and throw it into an array. We’ll need to cross reference this with our parent categories so that anything that doesn’t have both a Page and a parent category doesn’t show up. This allows us to have Pages that aren’t these special categories, as well as categories for organizational purposes that won’t be displayed in this navigation.
  4. Get the list of parent categories and store this in an array. I exclude categories like “Uncategorized” and some that I know I want hidden. I think the previous step would have taken care of this, but this code was written somewhat organically (re: my planned code looked nothing like this). During this step, also throw child categories into an array so we have the information for the breadcrumb later.
  5. Action time. We want to generate a simple linked list (just the list items right now, the outside ordered or unordered list tags can be done on the page itself) with our navigation links. So spit out a link with each parent category, and a link to the appropriate Page. When we get to the category that’s either our current category, or that category’s root parent, then we go nuts:
    1. If we’re a child category, then walk the ancestry tree down from parent to child to child’s child and put them each as a new unordered list and list item (we’re nesting here).
    2. Then we check for any child categories. We list each of our children, but ignore our children’s children and any progeny below them. If we have any, open one (and just one for this step, no matter how many kids) unordered list and then spit out the appropriate list items.
    3. Then we open up an unordered list if we didn’t do it in the last step, and spit out all the posts for our category. Then close the unordered list.
    4. Finally, close all the unordered lists we opened up for the breadcrumb navigation.

The code
Ok, enough yapping, here it is. I really am a total amateur at WordPress hacking. Same goes for PHP; I probably looked up every single function I used. So I’d love to see a cleaned up version of this. If I wasn’t back in my MBA program (classes resumed in full on Monday, hence my push to finish it before homework starts piling on) I think cleaning this up and making it an actual WP Plugin would rock. If you have suggestions for improvements, or just want to grab the below code and use it for yourself, other people, to actually rewrite as a plugin, etc, please do.

Finally, keep in mind that my permalinks are set up as /%category%/%postname%/. You will need to do the same for this code to have any hope of it working. Also, as stated above, you will need a Page for each category, and the slug for both the category and that Page must match exactly. If you have any questions or comments, feel free to leave me a note.

UPDATE: For your convenience, here’s the actual sidebar.php file. Note I added “.txt” to the end of it, just take that off. The bulk of this file is the hierarchical navigation (I don’t have much else in there save the login on the home page).

<?php
if ($cat) {
$current_categoryID = $cat;
} else {
$current_categoryID = get_page_category();
}
$pathToHome = get_option(‘home’); //the url to get to the root

$thisCat = get_category($current_categoryID);

//If we’re a child, create an array showing our tree ([0] is us, [1] our parent up to [x] a true parent)
if ($thisCat->category_parent != 0) {
$walkCat = $thisCat;
$ancestryCount=0;
while ($walkCat->category_parent != 0) {
$ancestry[$ancestryCount] = $walkCat->category_nicename;
$walkCat = get_category($walkCat->category_parent);
$ancestryCount++;
}
$rootParent = $walkCat->category_nicename;
$ancestry[$ancestryCount] = $rootParent;
}

$listOfPages = wp_list_pages(‘title_li=&echo=0&depth=1’);
preg_match_all(‘|a href=”([^”]+)|’,$listOfPages, $usedPageLinks);
preg_match_all(‘|>([^<]+)|’,$listOfPages, $usedPageNames);
$countPages=0;
while ($usedPageLinks[1][$countPages]) {
$tempPageLink = $usedPageLinks[1][$countPages];
$tempPageName = $usedPageNames[1][$countPages];
$tempPageLink = preg_replace(“|$pathToHome|i”,”,$tempPageLink);
$tempPageLink = preg_replace(‘|^/|’,”,$tempPageLink);
$pageNamesBySlug[$tempPageLink] = $tempPageName; // now we have a list of the parent pages
$countPages++;
}

$pathToCategories = get_category_link(9897); //this will fail if you actually have category #9897 …
$listOfCats = wp_dropdown_categories(‘echo=0&exclude=1, 2, 7&orderby=name’);
preg_match_all(‘/value=”(\d+)”>/’,$listOfCats, $usedCatIDs);
preg_match_all(‘|”>([^<]+)|’,$listOfCats, $usedCatNames);
$countcat=0;
while ($usedCatIDs[1][$countcat]) {
$tempCatID = $usedCatIDs[1][$countcat];
$tempCatLink = get_category_link($tempCatID);
$tempCatName = $usedCatNames[1][$countcat];
$tempCatLink = preg_replace(“|$pathToCategories|i”,”,$tempCatLink);
if ($pageNamesBySlug[$tempCatLink]) {
$parentPages[$tempCatLink] = $tempCatName;
$parentPageIDs[$tempCatLink] = $tempCatID;
$parentPagesOrderBy[$countcat] = $tempCatLink;
} else {
$childPages[$tempCatLink] = $tempCatName; //put the children into their own storage tank
preg_match_all(‘|^[^/]+|’,$tempCatLink, $thisChildParent);
$cppc=0;
while ($childPagesParents[$thisChildParent[0][0] . ‘/’][$cppc]) {
$cppc++;
}
$childPagesParents[$thisChildParent[0][0] . ‘/’][$cppc] = $tempCatLink;
}
$countcat++;
}
foreach ($parentPagesOrderBy as $ppob) {
echo ‘<li><a href=”‘ . $pathToHome . ‘/’ . $ppob . ‘”>’ . $parentPages[$ppob] . ‘</a>’;
if (($rootParent . ‘/’ == $ppob) || ((!$rootParent) && ($parentPageIDs[$ppob] == $current_categoryID))) {
//we’re at in our current category or its root parent
if ($rootParent . ‘/’ == $ppob) {
//oh, we must be a child. first we show our ancestry, working backwords:
$pathToChild = $pathToHome . ‘/’ . $ppob;
$pathToChild = preg_replace(‘|/$|’,”,$pathToChild);
$fullChildName = $ppob;
$fullChildName = preg_replace(‘|/$|’,”,$fullChildName);
$ancestryCount–; //we’ve just taken care of our root parent, so no need to repeat it.
$childCount = $ancestryCount;
while ($ancestryCount >= 0) {
$pathToChild .= ‘/’ . $ancestry[$ancestryCount];
$fullChildName .= ‘/’ . $ancestry[$ancestryCount];
if (!preg_match(‘|/$|’,$pathToChild)) {
$pathToChild .= ‘/’;
}
echo “\n” . ‘<ul><li><a href=”‘ . $pathToChild . ‘”>’ . $childPages[$fullChildName . ‘/’] . ‘</a>’;
$ancestryCount–;
}
}
// check for kids.
if ($childPagesParents[$ppob]) {
$haveAChild=0;
$myNiceName = $ppob;
foreach ($childPagesParents[$ppob] as $oneChild) {
if ($rootParent . ‘/’ == $ppob) { //we’re a child, so we need to make sure these aren’t siblings, us, uncles, etc.
if ($oneChild == $fullChildName . ‘/’) { //if the child is us, then skip it (in case we’re a child-parent)
continue;
} elseif (!preg_match(“|^$fullChildName|”,$oneChild)) { //it’s an aunt, uncle or sibling
continue;
}
$myNiceName = $fullChildName . ‘/’;
}
if (preg_match(“|^$myNiceName([^\/]+)\/(\w+)|”,$oneChild)) { //skip our grandchildren and younger
continue;
}
if (!$haveAChild) { //if this is to be our first child, then create an unordered list
echo “<ul>\n”;
}
echo ‘<li><a href=”‘ . $pathToHome . ‘/’ . $oneChild . ‘”>’ . $childPages[$oneChild] . ‘</a></li>’ . “\n”;
$haveAChild++;
}
}
if (!$haveAChild) { //we’ve had no kids, so start an unordered list
echo “<ul>\n”;
}

//then we show category posts: ?>
<?php
$postlist = get_posts(“numberposts=99&orderby=post_title&order=ASC&category=$current_categoryID”);
foreach ($postlist as $post) : setup_postdata($post); ?>
<li><a href=”<?php the_permalink(); ?>”><?php the_title(); ?></a></li>
<?php endforeach; ?>
</ul>

<?php
if ($rootParent . ‘/’ == $ppob) {
// need to close our unordered ancestry list
while ($childCount >= 0) {
echo ‘</li></ul>’ . “\n”;
$childCount–;
}
}

} //end of showing ancestry & posts
echo ‘</li>’ . “\n”;
}
?>

Comments closed.