Function Friday #15: display different navigation menus on child pages

Function Friday #15

Every Friday, I’m sharing code snippets that I use to customize WordPress. Feedback/suggestions are always welcome! For more information, check out the first post in the series.


On a massive site with lots of parent and child pages, you may not want to have a mega-menu with every single page in it. A possible solution is to conditionally display a second level of navigation, which contains different submenus depending on which page you’re viewing.

A quick way to do this could be to check whether you’re viewing a child page, and if you are, list all fellow child pages of its shared parent page. This doesn’t give you any control over the order, or let you exclude certain child pages.

Ideally you’d be able to create these various submenus under Appearance → Menus, giving you the same amount of control as your main menu. It would be tedious to have to go back to the code every time you want to add a new submenu, though.

What about dynamically creating menu locations based on your current page hierarchy?

The code

// Set up the menu array
$menus = array();

// Get all top-level pages
$top_level_pages = get_pages( array( 'parent' => 0 ) );

// Check each top-level page for children
if ( $top_level_pages ) {
    foreach ( $top_level_pages as $page ) {
        $children = get_pages( array( 'child_of' => $page->ID ) );

        // If this page has children, store a menu slug and name in the array
        if ( count( $children ) > 0 ) {
            $menu_slug = $page->post_name . '-menu';
            $menu_name = $page->post_title . ' Menu';
            $menus[ $menu_slug ] = $menu_name;
        }
    }

    // Create menu locations for each parent page using the array
    register_nav_menus( $menus );
}

Before you do anything else, create an empty array called “menus”, which we’ll use a bit later.

Now going through this code line by line… Unfortunately there’s no function in WordPress to simply get all pages that have children, so it takes a couple steps to find all parents. First, get all the top-level pages (i.e. pages that do not have a parent):

$top_level_pages = get_pages( array( 'parent' => 0 ) );

Then loop through them, one by one. With each page, try getting pages that have it as a parent – if the result is at least one page, you know you’re dealing with a parent:

foreach ( $top_level_pages as $page ) {
    $children = get_pages( array( 'child_of' => $page->ID ) );
    if ( count( $children ) > 0 ) {
        // We have at least one child! Which makes this page a parent.
    }
}

For each page that does have children, take its slug and its title and add the word “menu” to create the slug and title for your new menu location. Then store that slug and title in the $menus array you set up right at the start:

$menu_slug = $page->post_name . '-menu'; // Robot name (lowercase, no spaces)
$menu_name = $page->post_title . ' Menu'; // Human name
$menus[ $menu_slug ] = $menu_name;

Finally, take that array of menu slugs and titles, and plug it into the register_nav_menus function. This will create a menu location corresponding to each parent page:

register_nav_menus( $menus );

If your page hierarchy looks like this (where “About” and “Work” are the two pages that have children):

Page hierarchy

Your menu locations now look like this:

Menu locations

In this screenshot “Top Menu” and “Social Links Menu” are locations that came with the theme, Twenty Seventeen. The first two locations (“About Menu” and “Work Menu”) were dynamically created because those pages have children.

Switch to the “Edit Menus” tab, create menus for each of these new locations, and select the appropriate one under “Display location” (or come back to the “Manage Locations” tab and assign them there).

The last step is to display your new menus on the relevant page(s):

if ( $post->post_parent ) {
    // If the current page has a parent, get the parent's ID to grab its slug
    $slug = get_post_field( 'post_name', $post->post_parent ) . '-menu';
} else {
    // Otherwise, grab the current page's slug
    $slug = $post->post_name . '-menu';
}
// If a menu location with that slug has a menu assigned to it, display the menu
if ( has_nav_menu( $slug ) ) {
    wp_nav_menu( array( 'theme_location' => $slug ) );
}

To break this down a bit, the menu location slug will match the page’s slug, with the word “-menu” tacked on the end. On a page like “Contact” – which is a child page of “About” – you don’t want the slug “contact”, you want its parent’s slug (“about”) so you can display the menu that’s in the “about-menu” location:

if ( $post->post_parent ) {
    // If the current page has a parent, get the parent's ID to grab its slug
    $slug = get_post_field( 'post_name', $post->post_parent ) . '-menu';
} else {
    // Otherwise, grab the current page's slug
    $slug = $post->post_name . '-menu';
}

If there’s no menu assigned to a particular location, to avoid falling back to an incorrect menu, first make sure there actually is a menu in that location with the has_nav_menu function:

if ( has_nav_menu( $slug ) ) {
    // We have a menu at this location!
}

If there is, use the wp_nav_menu function to display it:

wp_nav_menu( array( 'theme_location' => $slug ) );

With this code in the header, the “Contact” page now correctly displays a second level of navigation, containing the menu assigned to the “About Menu” location:

Child page menu

The menu is not just listing the page’s fellow children of “About” – the possibilities are endless!

Where does it go?

The first block of code that registers your menu locations should go in your theme’s functions file. The second block of code, which actually outputs the menu, should go wherever you want this to happen – most likely in your header file, next to the main navigation. I wrote about where to put your customizations in the first Function Friday post.

Resources

Leave a Reply

Your email address will not be published. Required fields are marked *