32

(Moderators note: Was originally titled "wp_nav_menu Ancestor class without children in navigation structure")

I have a wp_nav_menu in my header which had three pages in it. When I am on one of those pages, the li containing that page in the menu gets the class .current_page_item. These three pages have templates, and these templates contain custom queries to get all posts of a certain content type. In effect, the perceived "children" of this top level page are not actually children, they're just of a content type I've associated with that top level page using a template.

I'd like the top level menu items to get a 'current-ancestor' class when the user is browsing a single page of a specific post type, again, associated with that page only in a custom query in the template file.

Hope that makes sense - if not, let me know where I lost you! Very much appreciate any help.

--Edited for specifics: For example, I have a static page called Workshops that is using a template. Its slug is workshops. The template has a custom get_posts function and loop within it, which pulls and displays all posts of a custom content type called workshops. If I click on one of these workshops' title, I'm taken to the full content of that piece of content. The permalink structure of the custom post type is set to workshops/postname, so as the user sees it, these pieces of content are children of the Workshops page, when in reality they're all of one content type but unrelated to the page. It's that gap that I need to effectively close in the menu, highlighting the 'Workshops' menu item when browsing content of type 'workshop'.

Again, hope that makes sense, I think I said 'workshop' upwards of 20 times in one paragraph!

3
  • @Gavin - Can you include a few more specifics of what you are trying to accomplish. It's easier to write an answer in concrete terms than if we try to do it in the abstract. Also if you could explain your URL structure related to these it would be helpful. Commented Oct 18, 2010 at 20:14
  • 1
    @Gavin - That helps. So your top level menu option is a list of workshops at "Workshops" with a path of /workshops/ and when a user is on a workshop page (i.e. /workshops/example-workshop/) you want the "Workshops" menu item to have the class current_page_item assigned, correct? Commented Oct 19, 2010 at 4:37
  • wp_nav_menu() exposes current-menu-ancestor class Commented Jun 13, 2011 at 21:06

7 Answers 7

30

There's a simpler solution. Forget creating pages for each post type just so you can have nav items, because as you have learned, WP has no way of recognizing that the custom types you are browsing are related to that page.

Instead, create a custom link in Appearance->Menus. Just put the URL that will return your custom type and give it a label, then press "Add to Menu".

http://example.com/workshops/

or non-pretty-permalinks:

http://example.com/?post_type=workshops

this alone will simply create a nav button which displays all posts with that custom post type, and will also add the current-menu-item class when you've clicked that nav item - but it won't yet add the nav class on any URL other than this one

Then, once it's created, go into the configuration for that new item, and enter the slug of the custom post type in the "Title Attribute" field (you could also use the description field, but that one is hidden in the admin screen options by default).

Now, you need to hook the nav_menu_css_class filter (which gets fired for each nav item) and check if the content being viewed is of the post type indicated in your custom nav item:

add_filter('nav_menu_css_class', 'current_type_nav_class', 10, 2 );
function current_type_nav_class($classes, $item) {
    $post_type = get_query_var('post_type');
    if ($item->attr_title != '' && $item->attr_title == $post_type) {
        array_push($classes, 'current-menu-item');
    };
    return $classes;
}

In this case, we're going to check that the Title Attribute field contents aren't empty and if they match the current post_type being queried. If so, we add the current-menu-item class to its class array, then return the modified array.

You could modify this to simply match the title of the nav item, but if for some reason you want to title the nav item differently than the plain slug of the post type, using the Title Attribute or Description field gives you that flexibility.

Now any time you're viewing a single item (or probably even archive listings) of a post type that matches a nav menu item, that item will be given the CSS class current-menu-item so your highlighting will work.

No pages or page templates needed ;-) The URL query takes care of fetching the right posts. Your loop template takes care of displaying query output. This function takes care of recognizing what's being shown and adding the CSS class.

BONUS

You can even automate the process using wp_update_nav_menu_item, by having menu items automatically generated for all your post types. For this example, you would need first to have retrieved the $menu_id of the nav menu you want this items added to.

$types = get_post_types( array( 'exclude_from_search' => false, '_builtin' => false  ), 'objects' );
foreach ($types as $type) {
    wp_update_nav_menu_item( $menu_id, 0, array(
        'menu-item-type' => 'custom',
        'menu-item-title' => $type->labels->name,
        'menu-item-url' => get_bloginfo('url') . '/?post_type=' . $type->rewrite['slug'],
        'menu-item-attr-title' => $type->rewrite['slug'],
        'menu-item-status' => 'publish'
        )
    );
}
4
  • That's the stuff! I'm using page templates only because the layouts are rather complex for those pages and don't just list the pages off, however I can still utilize that filter you provided to check the page ID. The nature of this theme is that the theme options get you to match up pages ('home' is this page, 'about' is this page, etc.), so that should work perfectly. Thank you for the (incredibly detailed) assistance!
    – Gavin
    Commented Oct 19, 2010 at 11:59
  • i had to remove the current_page_parent from the navigation item that was my blog - but otherwise it worked. thx Commented Oct 10, 2011 at 14:58
  • this was not working for me, since $item->attr_title pulled out the TITLE, and i wrote the title in capital letters. so i changed the attribute to $item->post_name and now it works fine for me.
    – honk31
    Commented Nov 29, 2013 at 17:42
  • I tried to make the code work for my theme, but can't get it work. There will no class applied to my parent item in the menu, when I am on the custom post type portfolio. I have used above code. What can be the problem?
    – Casper
    Commented Mar 25, 2014 at 15:51
4

instead of using

$post_type = get_query_var('post_type');

You might want to try:

$post_type = get_post_type();

As somtimes the post type is not set in the query var. This is the case for the default post_type of "post", so if you want to highlight a post that was listed from a listing page, you will need to use this. get_very_var() just returns an empty string for post types that aren't custom.

add_filter('nav_menu_css_class', 'current_type_nav_class', 10, 2 );
function current_type_nav_class($classes, $item) {
    $post_type = get_post_type();
    if ($item->attr_title != '' && $item->attr_title == $post_type) {
        array_push($classes, 'current-menu-item');
    };
    return $classes;
}
2

@Somatic - that's fantasic! I modified your code a bit so it also works for a specific Taxonomy (which I'm using only for the related post_type). The idea is to use the menu item's Title attribute to store both the name of the post_type AND the name of the taxonomy, separated by a semi-colon, and then exploded by the function.

add_filter('nav_menu_css_class', 'current_type_nav_class', 10, 2 );
function current_type_nav_class($classes, $item) {

    # get Query Vars
    $post_type = get_query_var('post_type');  
    $taxonomy = get_query_var('taxonomy');

    # get and parse Title attribute of Menu item
    $title = $item->attr_title; // menu item Title attribute, as post_type;taxonomy
    $title_array = explode(";", $title);
    $title_posttype = $title_array[0];
    $title_taxonomy = $title_array[1];

    # add class if needed
    if ($title != '' && ($title_posttype == $post_type || $title_taxonomy == $taxonomy)) {
        array_push($classes, 'current-menu-item');
    };
    return $classes;
}
2

Here my solution if you want to work with wp_list_pages.

add this in your functions.php

add_filter('page_css_class', 'my_page_css_class', 10, 2);
function my_page_css_class($css_class, $page){
    $post_type = get_post_type();
    if($post_type != "page"){
        $parent_page = get_option('page_for_custom_post_type-'.$post_type);
        if($page->ID == $parent_page)
            $css_class[] = 'current_page_parent';
    }
    return $css_class;
}

Now just add in wp_options table a new row with a option_name of page_for_custom_post_type-xxxx and a option_value with the page-ID u want to connect.

Perhaps you recognized that there is already a option called page_for_posts. If u only have 1 custom post type u can set your page at /wp-admin/options-reading.php in the dropdown and the navigation will set the current_page correctly.

I think wordpress core should extend this section with a dropdown for every post type registered.

2

I decided to stick with pages and use the page template name as a class on the nav item. This allows me to avoid cluttering up the title attribute which I didn't like about some of the other solutions.

add_filter('nav_menu_css_class', 'mbudm_add_page_type_to_menu', 10, 2 );
//If a menu item is a page then add the template name to it as a css class 
function mbudm_add_page_type_to_menu($classes, $item) {
    if($item->object == 'page'){
        $template_name = get_post_meta( $item->object_id, '_wp_page_template', true );
        $new_class =str_replace(".php","",$template_name);
        array_push($classes, $new_class);
        return $classes;
    }   
}

I also have body classes added to header.php

<body <?php body_class(); ?>>

Finally this solution requires some extra css to apply the selected/active state to your nav menu items. I use it to show taxonomy archives and custom post types related to the page as children of this page:

/* selected states - include sub pages for anything related to products */
#nav-main li.current-menu-item a,
body.single-mbudm_product #nav-main li.lp_products a,
body.tax-mbudm_product_category #nav-main li.lp_products a,
#nav-main li.current_page_parent a{color:#c00;}
2
  • This gave me the following error: Warning: join() [function.join]: Invalid arguments passed in /home/path/to/wp-includes/nav-menu-template.php on line 76 Any idea what happened here?
    – Jeff K.
    Commented Oct 21, 2011 at 0:04
  • Oh I think I see what's happening. It's because you are returning $classes within the if statement. Simply moving return $classes outside and after that if seems to solve the above error.
    – Jeff K.
    Commented Oct 21, 2011 at 1:14
1

@Somatic - Great code! I did make one change myself. I wanted to keep the Title Attribute for its intended purpose, so instead I placed the Custom Post Type slug in the Link Relationship (XFN) advanced menu properties that you can enable in Screen Options. I modified

if ($item->attr_title != '' && $item->attr_title == $post_type) {

and changed it to

if ($item->xfn != '' && $item->xfn == $post_type) {
0

Nice work Somatic.

Unfortunately, I dont get how you can list your custom post types in a page the way you explain. If I don't use a page-portfolio.php and add it to a page, all I get is 404 page.

If I do like Gavin does, I have modified you function a bit to also remove the "current_page_parent" from the blog page like this.

add_filter('nav_menu_css_class', 'current_type_nav_class', 10, 2);
function current_type_nav_class($css_class, $item) {
$post_type = get_query_var('post_type');

if (get_post_type()=='portfolio') {
    $current_value = "current_page_parent"; 
    $css_class = array_filter($css_class, function ($element) use ($current_value) { return ($element != $current_value); } );
}

if ($item->attr_title != '' && $item->attr_title == $post_type) {       
    array_push($css_class, 'current_page_parent');
};
return $css_class;

}

Not the answer you're looking for? Browse other questions tagged or ask your own question.