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”;
}
?>

29. January 2007 · Comments Off on WordPress 2.1 upgrade (or: sorry about the missing links) · Categories: WordPress · Tags: , , ,

Sorry, this blog is going hardcore nerd for a couple of days. I’m in the midst of WordPress hackery so y’all are getting subjected to it. A new cocktail very soon…

In the meantime, the Linky sideroll is back. It took me until today to realize it was gone. It turns out wp_get_links() may or may not have been deprecated in 2.1, but it now works differently and the codex doesn’t seem quite up to date. I had to make sure they all had a category, and then change the call from the old way (no parameters) to:

wp_get_links('category=4&before=<li>&after=</li>');

It’s possible I’m supposed to be using wp_list_bookmarks() instead, but frankly that doesn’t seem to have the options I need.

28. January 2007 · Comments Off on Using a single post as a home page in WordPress · Categories: WordPress · Tags: , , , , ,

UPDATE:
WordPress 2.1 allows you to set an page (not post) as your front page easily. Everything below is still valid if you need a post instead, but given the new built in functionality, using a page is now a preferred option. See here for details.

UPDATE #2:
Ok, so don’t do this. It only works until you have a enough posts that it would have moved your one post off the front page. Since your page may be limited to, say, 10 posts, if you’ve written 10 posts after this the hack below makes WP think it’s posted those ten, so it stops before getting to the post you want. Obviously updating the time-stamp is a workaround, but a silly one.

So I’ve been looking at perhaps moving The Center for Effective Discipline’s website to use WordPress for the back-end. The site is huge, and needs to be moved to either a CMS system or WordPress. I’ve heard about speed and complexity issues with CMS systems, so right now we’re going down the WordPress avenue.

Anyhow, using WordPress 2.1 I wanted to use one post as the homepage. I could have easily just downloaded Semiologic’s Static Front Page Plugin, but then I would have had to use a page. I’m looking at using pages for navigation, so while that’s still an option, I went the post route instead. This requires a few changes.

I ended up basically just creating a post in its own category, and then limiting the homepage to just that category. These changes are probably hackier than you need, but for future reference and in case someone else is trying the same, here’s what I did:

  1. Create a category or use one that only this post will be in. You’ll need the ID for this category (you can see it in Site Admin -> Manage -> Categories). I used “Uncategorized”, which is ID 1, since all articles on this site going forward need to be in an existing category.
  2. Then you need to change the index.php file for whatever theme you’re using. Here is my file, with my additions in bold:

    < ?php get_header(); ?>
    <div id="content" class="narrowcolumn">
    < ?php if ($posts) { ?>
    < ?php foreach ($posts as $post) : start_wp(); ?>
    < ?php if ( (in_category('1')) || !is_home() ) { ?>
    < ?php require(TEMPLATEPATH. "/post.php");?>
    < ?php } ?>
    < ?php endforeach; ?>
    < ?php if(!is_home()) { ?>

    < ?php posts_nav_link(' — ', __('« Previous Page'), __('Next Page »')); ?>

    < ?php } ?>
    < ?php } else { ?>
    (... rest of file)

    This boils down to adding two “IF” statements. The first says “only display posts from category 1 if this is the home page.” The second says, “if we’re on the home page, don’t put navigation aides (like “next post”) even if WordPress thinks it needs them.” That second IF statement is needed because WordPress thinks it’s about to display all your most recent posts, only when it’s about to display them we told it to filter based on the first IF statement. By the time the code gets down to the navigation links, it doesn’t know that we filtered the posts and thinks it has displayed lots and lots. As such, if you’re using this hack to just display one category on the home page, don’t use the second IF statement!

  3. That’s it. Save and upload the new index.php file and you should be good to go.

Another post on how to create a hierarchical navigation system (that shows categories and all of the posts in the category you’re currently in) in the sidebar is coming soon. Try not to dance in the streets with excitement.