Current section navigation in WordPress

Increasingly, we’re building sites – like our own – that are completely managed by WordPress. WordPress makes it very easy to output a list of pages using the wp_list_pages function; you can control depth levels, surrounding tags, and even explicitly exclude certain pages. And with the precise classes that accompany the output HTML elements, you can finely control the output with stylesheets.

Many sites, also like ours, list pages only under the current top level page (or, put another way, pages in the current section) in a sidebar. For example, if you go to the Services page, or any of the pages inside that section, you’ll notice a box atop the right sidebar called “More in Services” that provides navigation within the Services section.

Taking advantage of the wp_list_pages function to create section-based navigation for sites with two levels of page depth is a pretty straightforward matter, as I’ll explain. But how do we efficiently output section based navigation for sites with 3 or more levels of page depth?

The wp_list_pages function allows developers to list subpages (also known as “child” pages) of a given page through a “child_of” paramater that accepts a page ID. So, for example, our Services page has an ID of “4″. If we want to list only the subpages of the Services page, without a heading, we can use the following call:

wp_list_pages('title_li=&child_of=4');

If we want to automatically list the children of the current page, we can call the post ID, and write the function like so:

wp_list_pages('title_li=&child_of='.$post->ID);

What if we’re on a child page? How do we list its siblings? Fortunately, WordPress provides an easy method to get a page’s parent ID. Here’s the answer:

wp_list_pages('title_li=&child_of='.$post->post_parent);

But how do we know if need to show siblings or children? Let’s assume for the moment we only have two levels of pages, as this site has: top level pages, i.e. Services, and subpages, i.e. Areas of Focus. All we have to do is check whether the page actually has a parent, and then choose our function accordingly.

if ($post->post_parent) {
    wp_list_pages('title_li=&child_of='.$post->post_parent);
} else {
    wp_list_pages('title_li=&child_of='.$post->post_ID);
}

In most real world scenarios you’ll probably want to confirm that there are actually pages to list (you can check whether wp_list_pages actually returns a value if you add “echo=0″ to the parameters) and that we’re actually on a page (is_page()). But we won’t get into that now.

But what about sites with 3 levels (or more) of pages? Just yesterday I was developing a template for a client that called for a section navigation in the sidebar; but this site had at least 3 page levels. The idea was to show all subpages within the current section, no matter what level of page depth.

There were, of course, a few ways to approach the challenge. One could, for instance, output the entire navigation again, and use the precise  element classes (i.e. “current_page_item”) to only show the appropriate pages. But that seemed wasteful, and the redundant information probably not ideal for search engine optimization.

As it turns out, WordPress just recently (as of version 2.5) added a barely documented get_post_ancestors function that, when passed a page, will return an array with the ancestory of that page, going back as many levels (pages) as necessary, with each page’s ID along the way. The last item in the array is the (drumroll) top level page.

A little basic PHP array code, and you can quickly determine whether you’re actually on a sub page (at any level), pull out the top level page ID if so, and pass it to the wp_list_pages function to get the desired section navigation. You can also get the title of the top level page, a.k.a the section you’re in.

$post_ancestors = get_post_ancestors($post);
if (count($post_ancestors)) {
    $top_page = array_pop($post_ancestors);
    $children = wp_list_pages('title_li=&child_of='.$top_page.'&echo=0');
    $sect_title = get_the_title($top_page);
} elseif (is_page()) {
    $children = wp_list_pages('title_li=&child_of='.$post->ID.'&echo=0&depth=2');
    $sect_title = the_title('','', false);
}
if ($children) {
    //** however you want to output your section navigation **/
}

Voíla!

UPDATE: Outputting only appropriate grand children and lower

If you read the comments below, Daniel had a great question about the display of grandchildren and lower. I’ll let his post explain the question in detail, but the general idea is that often times we only want to show grandchildren (3rd level pages or lower) if we’re inside the corresponding 2nd level page’s navigation. As I explained to Daniel, there’s an easy way to do this with CSS, which is what I had used, but if you’re determined to output, in HTML, the appropriate pages, here’s the answer:

//get the current page's ancestors
$post_ancestors = get_post_ancestors($post);

//initialize pagelist variable to hold list of pages to include
$pagelist = "";

//add the immediate children (no grandchildren) of each page
//in this page's ancestory to the list
foreach ($post_ancestors as $theid) {
     $pageset = get_posts('post_type=page&post_parent='.$theid);
     foreach ($pageset as $apage) {
          $pagelist = $pagelist.$apage->ID.",";
     }
}

//add any children of the current page to the list
$pageset = get_posts('post_type=page&post_parent='.$post->ID);
foreach ($pageset as $apage) {
     $pagelist = $pagelist.$apage->ID.",";
}

//get the list of pages, including only those in our page list
$children = wp_list_pages('title_li=&echo=0&include='.$pagelist);

//get the top level ancestors title, aka the section title
$sect_title = get_the_title(array_pop($post_ancestors));

If you followed that, I probably don’t need to spell this out, but you’ll want to wrap that code in a more basic check or two, depending on your template (is_page(), whether it has a parent, etc) as illustrated in the previous example’s code.

As I mentioned in my comment to Daniel, this took a couple of hours to figure out. So if this helps you with a project, we’d humbly ask that you donate whatever you feel this was worth!

39 Responses to “Current section navigation in WordPress”

  1. Daniel says:

    Here’s an interesting dilemma. I’ve tried this method and it’s certainly closer to what I’m looking to achieve. I made an exception to the depth as I wouldn’t want subchild-pages (grandchildren) to display while navigating any main-page. I’ve set the depth to 1 and this takes care of that preference.

    The problem I have currently, however, is whether it’s possible to only display children of a subpage (grandchildren) when actually on a child page. Represented as follows:

    If on the page Subpage 1, the following list will display:

    –Subpage 1
    –Subpage 2
    –Subpage 3

    If on Subpage 2 (that has children pages), the menu will display accordingly:

    –Subpage 1
    –Subpage 2
    —Sub Subpage 1
    —Sub Subpage 2
    –Subpage 3

    Consequently, if on other subpages such as Subpage 1 or 3 (which do not have children pages), the menu will display as follows:

    –Subpage 1
    –Subpage 2
    –Subpage 3

    I certainly like the flexibility of only presenting relevant children pages while on the specific subpage, in other words. Otherwise, we end-up with very long navigation menus and it breaks the semantic logic of information architecture when items not relevant are displayed.

  2. Jake Goldman says:

    Daniel,

    Great question. We’re doing that exact kind of output for the site we’re working on right now.

    However, considering the limited number of “grandchildren” within a section, we were happy to let CSS do the rest of the work. Here’s how.

    Put the navigation in a box with an identifiable ID (let’s say “sidenav”). Set the “depth” value back to 2 (show children and grandchildren). Now let’s use our stylesheet to, by default, not display grandchildren:

    #sidenav li ul { display: none; }

    Now, WordPress will give the li for the current page you’re on a “current_page_item” class. Parents above the page you’re on get a “current_page_ancestor” tag. So add this to your stylesheet to accomplish what you describe above:

    #sidenav li.current_page_item ul, #sidenav li.current_page_ancestor ul { display: block; }

    There’s probably a more clever way using the API to only show the relevant page, but assuming there aren’t a very large number of grandchildren page, the CSS is probably more efficient. I’ll think about it when I have a little more time, however, and let you know if I come up with something more clever!

  3. Daniel says:

    Thank you for the reply, Jake.

    I had acheived the CSS option as well, and it’s an easy method. However, I suppose I’m interested in how to lock such a schema down through the API, because the document would render with every page in the lists if the CSS file was not being read [eg: some use cases where CSS or alternative stylesheets are overriding site and designer preference, readers, scanners, mobile, etc].

    It would definitely be a neat thing if somebody finally figured this one out in its most elegant form, as I am a fan of elegance.

  4. Jake Goldman says:

    Daniel,

    OK, I’ve got an answer for you! This one actually took a couple of hours to figure out (yikes), but you piqued my curiosity. If this helps you with some projects, I’d humbly ask that make whatever contribution you feel is justified (just use the donation link I’ve added below the full post).

    Rather than load up the comment, I’m going to update the post. Hang on a minute. :-)

  5. Daniel says:

    For some reason it’s not parsing out anything. Is there something I could be missing?

  6. Jake Goldman says:

    Daniel,

    Can you clarify? We think there may be an issue with a hierarchy being preserved on output (as opposed to a flat LI list), but you should definitely get some output.

    This may be obvious, but keep in mind nothing in the code actually does an echo; you’ll need to echo the “$children” variable to get output.

    If you echo children at the end, you get nothing?

  7. Daniel says:

    Sorry, I couldn’t connect to the site for some time. I did add the echo and everything parsed as it should. What I did run into as a problem was in needing a conditional to parse out lists only for pages with subpages, else a list would parse on other pages that included the entire file list. Add the following rendering function and all works as it should:

    if ($children && $pagelist) {
    $sect_title = the_title('

    ','

    ', true);
    echo '
      ', $children, '
    ';
    }

  8. Daniel says:

    I’m still up in the air a little bit about what level should render when drilling into the sub-level pages. I think we’re on the right track to coming up with the best CMS navigation for WordPress though!

  9. Jake Goldman says:

    Great catch, Daniel, on the conditional.

    Can you explain your uncertainty about which levels should render?

    Might have to turn this into plug-in at some point!

    Sorry about the down time, earlier.

  10. Daniel says:

    Quite okay. Happens on my sites occasionally as well.

    I suppose what I’m thinking, is that the parent hierarchy should only display for a subpage section when on a subpage and sub-subpage.

    First level (main page current), as it currently works on first level:

    (Main page current)
    Page 1
    Page 2
    Page 3

    First subpage level (child page) display when a page with subpages is clicked:

    Page 1
    Page 2 (current)
    -Subpage 2.1
    -Subpage 2.2
    Page 3

    Second level subpage (grandchild) with another child:

    Page 2
    - Subpage 2.1
    - Subpage 2.2 (Current)
    –Subpage 2.2.1
    –Subpage 2.2.2

    Third level:

    -Subpage 2.1
    -Subpage 2.2
    –Subpage 2.2.1 (Current)
    –Subpage 2.2.2

    And so on…

    I feel this would maximize real estate and keep only the parent structure more relevant. In a way, this also acts as a to-from navigation scheme arguably more elegant than a breadcrumb breadcrumbs are of course only from navigation schemes to backtrack a placement in the hierarchy).

  11. Jake Goldman says:

    I think I follow (yikes). Tell me if this makes sense:

    *If* a page has children … you want to show its siblings and children, with its parent as the heading (sect title) for the navigation block.

    *If* a page does *not* have children … you want show its siblings, its parent, and its parents’ siblings.

    Personally I think this could be a bit more confusing for users than our original concept… however, I can see the potential use case.

    It seems you want the side navigation on (as WordPress refers to it) your “front page”, but you don’t need those top level pages to be in the side navigation once they’re well within those sections… ?

  12. Daniel says:

    I suppose the best way to diagram it would be:

    Parents Level Current Page Level Siblings for Current Page

    As you drill down then, you retain the same backward/forward structure — if this makes sense?

  13. Daniel says:

    Sorry, had some brackets in there:

    Parents – Current Page Level – Siblings for Current Page

  14. Daniel says:

    A little follow-up. I ran into an issue. Apparently, only 5 subpages in the hierarchy will display, for pages that only contain subpages (and no children of subpages). Sort of interesting – not sure what the work-around is.

  15. Jake Goldman says:

    Daniel — sorry for the belated response.

    It looks like there’s also a “numberposts” variable in the “get_posts” function. It defaults to (wait for it)…. 5.

    Just set it to some high number like 10, like so:

    get_posts(‘post_type=page&post_parent=’.$theid.’&numberposts=15′);

  16. Daniel says:

    Thank you, Jake. I’ll have to look into a way to see if I can get it to list everything by default. It’s strange they’d default that to 5 actually — wondering if anyone reported that to Trac.

  17. Daniel says:

    Success! Apparently they already provisioned it in.

    Reference: http://codex.wordpress.org/Template_Tags/get_posts

    $numberposts
    (integer) (optional) Number of posts to return. Set to 0 to use the max number of posts per page. Set to -1 to remove the limit.
    Default: 5

  18. Jake Goldman says:

    Daniel – yep, you can use -1 to show all.

    In terms of “why the limit”, keep in mind two points:

    (1) get_posts will return *all* the post data (the content, custom fields, etc). You want to be careful (memory and performance wise) about pulling in, say, 100 blog posts.

    (2) The typical use case is not navigation building, but actually looping through posts, much like the main loop. For example, in a special home page template that has content of its own, you may want to highlight the 3 recent blog entries from a “featured” category, and loop through them like you would on the blog index or archive. In most situations, you won’t want to pull in more than a handful of posts.

  19. Faisal Khan says:

    Jake, you just created a solution, which blows one million plugins, one billion work arounds and one trillion wp-haters whining out of the water!

    I just cant believe why this solution is not yet found! This should be like a Golden-Egg for any Wordpress CMS solution …

    Now, only I should also contribute something! :-P

    Very well done!

    P.S. Would this great testimonial do instead of the contribution. I am willing to do both thought . . . :-D hahah! (jk)

  20. Jake Goldman says:

    Faisal,

    Thanks so much for the compliment!

    We think WordPress makes a great CMS, but you do have to be willing to dig into the API. We’re thinking of making a “side navigation” plugin / widget based on this code, to help less advanced developers out.

    A donation would be terrific, especially if it helps you out with a paying project. As we tell everyone, the best way to thank us is to refer some work our way!

  21. Faisal Khan says:

    Definitely!

    btw, you should check your site in Chrome.

    The Main text is quite blurry …

  22. Jake Goldman says:

    Faisal – I’m looking at the site right now in Chrome, and it doesn’t look blurry to me. Can you email us a screenshot of what it looks like for you?

  23. RaiulBaztepo says:

    Hello!
    Very Interesting post! Thank you for such interesting resource!
    PS: Sorry for my bad english, I’v just started to learn this language ;)
    See you!
    Your, Raiul Baztepo

  24. Fred says:

    I get a weird order when I use this – the subnav shows up first in the unordered list.

    Sub Nav
    Top Level 1
    Top Level 2

    It would be great if it could do this:

    Top Level 1
    Sub Nav
    Top Level 2

    Thoughts?

  25. Fred says:

    Scratch that – works perfect!

  26. Fred says:

    What would be great would be to be able to stick the sub-items in a nested unordered list.

  27. Jake Goldman says:

    Fred – I’m a bit confused. The sub-nav items aren’t nested within their top level page? They should be… out of curiosity, have you tried our plug in? Are you having the same not-nesting issue?

  28. Fred says:

    Yes. Screenshot using plugin. The top two items are actually sub-items of “Volume 1″.

  29. Ray says:

    This looks exactly like what I want to do, however I need it for posts and not pages… any plans on releasing a “posts” version?

  30. Jake Goldman says:

    Ray – can you explain what you’re thinking for posts? Are you referring to a hierarchical category navigation… ?

  31. Ray says:

    Hi Jake,

    Hierarchical category navigation is what I meant.
    Sorry for the confusion!

    I know categories would be a little bit more difficult.

  32. Roger says:

    Hi Jake,

    If somehow from the admin files, we make the default value of the page box as 1 instead of Zero, the problem is solved.

    Like with these lines.
    menu_order;
    if ($orderMenu ==0)
    $orderMenu =1;
    ?>

    My comments deleted ? :-\

  33. Roger says:

    Hi Jake,

    Did you manage to solve the Order problem of the plugin?

    Many Thanks.

  34. Wordress sub-menu navigation | Benjamin Ashcroft says:

    [...] and very flexible. Yet finding out the depth of the page seem to be a dead end. Until I read this tutorial by Jake Goldman talking about under document but great function, called [...]

  35. Sia says:

    You saved my life. Thank you thank you thank you….

  36. Sean says:

    Alright, so your solution seems awesome and gets me about 75% of the way to my goal… here’s the question I have:

    I have PARENTS, CHILDREN, and GRANDCHILDREN. I have my site correctly displaying what I want on CHILDREN (just siblings), but on GRANDCHILDREN I want just the immediate parent page (CHILD) and that page’s siblings (CHILDREN) displayed.

    Is there a way to exclude the current page and it’s siblings from the array if it’s a GRANDCHILD?

    Thanks!

  37. Jake Goldman says:

    Sean – the short answer is yes. You can get the parent page with get_post($post->post_parent) and siblings (without self) with get_pages(‘parent=’.$post->post_parent.’&exclude=’.$post->ID)

  38. Sean says:

    Jake: That gets me part of the way… I need to test for the level it’s on (so 3rd level aka grandchild) and then only display the 2nd level (aka child) pages. How can I edit the if/else statement to add a third option for this 3rd level down?

    Thanks!

  39. Sean says:

    I was actually able to solve my problem with the “Exclude Pages” plugin… just FYI for anyone else looking for a solution too.

    Thanks again for the help Jake!

Leave a Reply