4

I'm looking for ways to prevent "private" and "draft" pages from appearing (to non-logged-in admins) in a site's Primary Navigation menu which is based on a custom menu - other than manually removing such pages from the WP custom menu and then dragging them back when they are published, that is.

FWIW, I'm surprised the behavior I'm looking for isn't the default.

Using: WP 3.4.2

4
  • To clarify: what code are you using to output your "custom menu"? Oct 11, 2012 at 18:58
  • None (if I understand your question). This happens using the current Twenty-Ten theme (v1.4) with no customization - other than having created a Custom Menu in the "Appearance" subpanel and assigning it to Primary Navigation in the "Theme Locations" section.
    – Jeff Cohan
    Oct 11, 2012 at 19:36
  • 2
    Here is a solution that filters drafts from wp_nav_menu: wordpress.org/support/topic/…
    – user24720
    Dec 12, 2012 at 11:04
  • 1
    There's now a plugin for that: Hide Drafts in Menus
    – Guss
    Oct 28, 2019 at 21:21

6 Answers 6

6

I had a similar problem and here is the best solution I could come up with. The reason (I think) that private or non-published items show up in menus is that the menu items are themselves posts and have their own post_status. That means that in a situation where a page is marked private, the menu item for that page can still be set to publish, and is displayed. I wrote a filter callback for the wp_nav_menu_objects hook which looks at the post_status for the object that the menu item stands for, and removes it from the menu if that object is private. In my case, private pages was the concern, but one can easily adapt this for drafts. I also allow the menu items to be displayed if the user is logged in, whatever his capabilities are, whereas WP by default limits access to a private page to administrators and editors. This also can easily be adapted.

function jj_filter_private_pages_from_menu ($items, $args) {
    foreach ($items as $ix => $obj) {
        if (!is_user_logged_in () && 'private' == get_post_status ($obj->object_id)) {
            unset ($items[$ix]);
        }
    }
    return $items;
}
add_filter ('wp_nav_menu_objects', 'jj_filter_private_pages_from_menu', 10, 2); 

It seemed to me easier and more appropriate to filter the menu items than to hide them with CSS, your milage may vary.

To limit menu items for private pages to administrators and editors you should be able to substitute current_user_can ('read_private_pages') for !is_user_logged_in ().

1
  • 1
    Many thanks for this. This is such a better answer than the css hacks. This logic already exists in the core of wordpress when actually viewing the pages, so I am unsure why they left it out of the menu logic.
    – Phil_1984_
    Sep 18, 2015 at 9:27
5

I used this as a solution for hiding draft pages. You can hide the (draft page) menu items with the li.draft class.

add_filter('nav_menu_css_class' , 'nav_menu_add_post_status_class' , 10 , 2);
function nav_menu_add_post_status_class($classes, $item){
    $post_status = get_post_status($item->object_id);
    $classes[] = $post_status;
    return $classes;
}

FWIW: I feel the same way ;-)

3

The easiest solution is to add a specific class to the menu items that you want to hide. And then hide them through CSS.

custom nav menu classes
^ click to enlarge ^

The CSS classes are not visible by default, you have to enable it in the Screen Options upper tab.

If your theme does not print the relevant classes in the <body> tag, this will do:

add_filter( 'body_class', 'wpse_67929_body_class' );

function wpse_67929_body_class( $classes ) 
{
    if( !is_user_logged_in() )
        $classes[] = 'not-logged-menu';

    return $classes;
}

Finally, in your style.css, add this rule:

body.not-logged-menu .menu-logged-in {display:none}

Another option, use the following filter:

add_filter('wp_nav_menu_items', 'wpse_67929_nav_menu_items', 10, 2 );

function wpse_67929_nav_menu_items( $nav_menu, $args )
{
    // Manipulate the output
    // $nav_menu contains a single string with the menu Html structure
    // more or less like this: 
    // <li id class><a href>Menu Item</a></li> <ul><li><a>Submenu Item</li></ul> <etc>
    return $nav_menu;
}

"Manipulate the output" seems simple but reveals to be tricky:

2

Hiding them with CSS is problematic, as they still get sent over the wire and still expose what could be sensitive information to anyone who looks at the source, to those who disable CSS for whatever reason (e.g. blind users — CSS just slows them down to no real benefit), and to hacker scripts and the like.

No, we need a way to allow menu items to be added to a menu that reference a page or post, but to only show or even send those menu items over the wire if the page or post to which they refer has been fully and publicly Published. Not Draft, not Pending, not Private, not Scheduled (until the scheduled time has passed), and certainly not Trashed.

Here’s a solution I found at another place which I modified slightly (add to theme’s functions.php):

function exclude_draft_nav_items( $items, $menu, $args )
{
  global $wpdb;

  //add your custom posttypes to this array
  $allowed_posttypes = array( 'post', 'page' );

  $sql = "SELECT ID FROM {$wpdb->prefix}posts WHERE (post_status!='publish') AND ID=%d && post_type=%s";

  foreach ( $items as $k => $item )
  {
    if( in_array($item->object, $allowed_posttypes) )
    {
      $query = $wpdb->prepare( $sql, $item->object_id, $item->object );
      $result = $wpdb->get_var( $query );

      if( $result ) unset($items[$k]);
    }
  }

  return $items;
}

add_filter( 'wp_get_nav_menu_items', 'exclude_draft_nav_items', 10, 3 );
1

It would be much better to make one call to the database instead of multiple queries, especially if the menu has several items. Based on some of the other answers you could build a list of approved ids and filter the items with that. Please note that the array declarations are PHP7.0+

function exclude_draft_nav_items( $items, $menu, $args ){
    global $wpdb;
    $ids = [];
    $out = [];
    foreach( $items as $k=>$v ){
        $ids[] = $v->object_id;
    }
    $ids = join( ',', $ids );
    $res = $wpdb->get_results("SELECT ID FROM $wpdb->posts WHERE ID IN ($ids) AND post_status = 'publish'");
    $ids = [];
    foreach( $res as $v ){
        $ids[ $v->ID ] = true;
    }
    foreach( $items as $v ){
        if( !isset( $ids[$v->object_id] ) ){
            continue;
        }
        $out[] = $v;
    }
    return $out;
}

add_filter( 'wp_get_nav_menu_items', 'exclude_draft_nav_items', 10, 3 );
0

Ok guys, Im here to give the best answer. This should be in WordPress core by now but I guess they have more important things to deal with.

The top rated answer from 2015 is good, but the below code is better in that it uses the wp_get_nav_menu_items function to retrieve all menu items of a navigation menu. Also, there is no need to check if !is_user_logged_in if you want a menu item hidden...you're just making people waste time doing an incognito search if they're logged in. Also, there is no need to do a search on the database

function hide_menu_items_for_draft_pages( $items, $menu, $args ) {
    foreach ( $items as $key => $item ) {
        $post_status = get_post_status( $item->object_id );
        if ( $post_status == 'draft' ) {
            unset( $items[$key] );
        }
    }
    return $items;
}
add_filter( 'wp_get_nav_menu_items', 'hide_menu_items_for_draft_pages', 10, 3 );

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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