No draft in menu?

Have you ever tried to set a WordPress page to draft status, after you added it the main menu? I did this recently and expected WordPress to be smart enough to also update the main menu to reflect that change.

However, when I viewed the webpage as a guest, I could still see the menu item. When I opened that page WordPress only displays a 404 not-found message. Which is technically correct; but wouldn’t it be a lot better, when guest users would not even see the menu items for draft posts?

The complicated way

Obviously, WordPress wants us to modify the post state and separately update the menu (remove the page from the menu manually). Possibly this is a good idea for caching – but on the other hand, most caching is usually disabled for logged in users anyway.

A more elegant way

Or maybe this is just a lazy way.

I took a look into the file nav-menu-template.php in search of a solution that does the redundant work and modifies the main menu. We hook into the filter wp_nav_menu_objects so we can remove inaccessible menu-items right before they are displayed!

Actually, it’s a straight forward process, once we found the correct hook and understand the filter-parameter:

add_filter( 'wp_nav_menu_objects', 'pst_nav_menu_objects', 10, 2 );

 * Modify the WordPress menu and remove entries that are not visible for the current
 * user. This applies to all menus (primary, footer, widget ...)
function pst_nav_menu_objects( $items, $args ) {
    // If you do not want to modify ALL menus, you can check for the menu-location 
    // or other criteria here.
    // For example, uncomment following condition to only modify the primary menu:
    # if ( 'primary' !== $args->theme_location ) {
    #     return $items;
    # }
    foreach ( $items as $key => $item ) {
        if ( ! in_array( $item->object, array( 'post', 'page' ) ) ) {
            // We only check visibility state for posts and pages.

        $post_status = get_post_status( $item->object_id );
        if ( 'publish' === $post_status ) {
            // This is a public page or post. No other check is needed.

        if ( is_user_logged_in() ) {
            if ( 'protected' === $post_status ) {
                if ( current_user_can( 'edit_post', $item->object_id ) ) {
                    // The current user is the author of the password protected post.
            } else {
                if ( current_user_can( 'read_post', $item->object_id ) ) {
                    // The current user can read the draft or trashed post.

        // All positive conditions failed.
        // Current visitor has no permission to see this menu entry.
        unset( $items[ $key ] );

    return $items;