WordPress tutorial: Custom Menu For Posts or Pages in the Sidebar

This post is for you who manages a WordPress site that has a lot of content, possibly many pages structured in hierarchy, and wish for better navigation outside the main menu. In order to help navigate the site a custom menu related to the current post will help tremendously. The problem with placing a menu widget in the sidebar (or wherever you like) is that the sidebar is common. In this post we’ll learn how to show an additional custom menu in the sidebar, by allowing posts, pages or custom post types to choose a menu.

Add the code below in your theme’s functions.php or inside your plugin code.

Allow posts or pages to choose a menu

Creating menus in WordPress is easy, and you can use the Menu widget to display a menu in the sidebar. The problem is that the sidebar is global and the same menu will be displayed everywhere. What if you’d like specific menus to be shown on specific pages? You’ll also learn how you can make sure the chosen menu is inherited by the child pages. That way you only need select the menu on the parent page. Any subpages will also show the same menu without needing to edit them all.

Adding a metabox for choosing menu

The first step is creating a metabox on posts or pages where we get the option to choose a menu. We use the function add_meta_box(), and decide what post types we want to show it for.

add_action('add_meta_boxes', function() {
	add_meta_box('metabox-sidebar-menu', __('Sidebar Menu', 'txtdomain'), 'awp_sidebar_menu_metabox_callback', ['post', 'page']);
});

Adjust the above code to the title and post types you want. The above example will add the metabox to both posts and pages. The third parameter, what I’ve called awp_sidebar_menu_metabox_callback, is the function responsible for rendering the metabox’s content. Let’s define that next. This is what we’ll need to do in our metabox:

function awp_sidebar_menu_metabox_callback($post) {
	// Get all menus

	// Get the current saved menu, if set

	// Output HTML with a select showing all menus, and mark the currently saved one as selected
}

We can get an array with all saved menus in WordPress with wp_get_nav_menus(). As for fetching current chosen menu we store the chosen menu as a post meta in awp_sidebar_menu (call it whatever you’d like), and we’ll simply fetch the value based on the current $post provided to us in the metabox function. We will save menu IDs because that’s all we need in order to display a menu. And then we output HTML for a select that loops through the menus. The HTML output of the metabox is really up to you, the below is an example. I’ve included nonce functionality for security as well.

function awp_sidebar_menu_metabox_callback($post) {
	// Get all menus
	$menus = wp_get_nav_menus();

	// Get the current saved menu, if set
	$current_selected = get_post_meta($post->ID, 'awp_sidebar_menu', true);

	// Output HTML with a select showing all menus, and mark the currently saved one as selected
	wp_nonce_field('awp_sidebar_menu_metabox_nonce', 'awp_sidebar_menu_nonce');
	?><div class="awp-metabox-item">
		<div class="awp-metabox-label"><label><?php _e('Choose menu', 'txtdomain'); ?></label></div>
		<div class="awp-metabox-input"><?php
		if (empty($menus)) {
			echo '<p>' . __('No menus created.', 'txtdomain') . '</p>';
		} else { ?>
			<select name="awp-sidebar-menu" id="awp-sidebar-menu">
				<?php 
				echo '<option value="">' . __('Choose menu', 'txtdomain') . '</option>';
				foreach ($menus as $menu) { 
					echo '<option value="' . $menu->term_id . '" '.selected($current_selected, $menu->term_id).'>'.$menu->name.'</option>';
				} ?>
			</select>
		<?php } ?>
		</div>
	</div><?php
}

In the HTML output I’m printing out a label. If there are no saved menus in WordPress at all, it will simply display a paragraph. Otherwise a select is generated with menu IDs as values and menu names as label. I’m also adding an empty choice to allow posts to not show a menu. I’m using WordPress’ helper function selected() to handle marking the current saved option as selected.

If you edit a post or page you should see the metabox appear at the bottom, showing your select. Awesome! However at this point it will currently not save your menu choice when you save the post. That’s the next step.

Saving the menu choice

We use the hook save_post to create a function that saves any choice we’ve added in our metabox. The save_post hook is triggered every time a post is being saved or updated. We’ll check nonce first (if you are unsure what nonces are, check this WordPress guide about nonces). We then doublecheck if the user is allowed to update posts, and updates our post meta with the choice.

add_action('save_post', function($post_id) {
	if (!isset($_POST['awp_sidebar_menu_nonce']) || !wp_verify_nonce($_POST['awp_sidebar_menu_nonce'], 'awp_sidebar_menu_metabox_nonce')) {
		return;
	}

	if (!current_user_can('edit_post', $post_id)) {
		return;
	}

	update_post_meta($post_id, 'awp_sidebar_menu', $_POST['awp-sidebar-menu']);
});

Now when you update posts, it will also save your choice of menu as well.

And that’s it for the post choice part. The next step is actually outputting the menu if a menu was selected.

Choosing a position for the custom menu

I’m adding the output in the sidebar, but you can output it anywhere in your theme’s templates. We just need either a predefined hook or define our own. As an example I’m adding a custom hook at the top of the sidebar, so that I can create a function hooked to this.

You could simply call wp_nav_menu() directly in the template but I recommend creating a custom hook instead because we will add quite a bit of code and it can appear messy.

In my theme I edit sidebar.php and right before dynamic_sidebar() with the sidebar is called (where widgets are added), I add my custom hook with do_action() and a given name. You can call it whatever you’d like, but it must be unique within WordPress. So at least prefix it with something unique for you.

<aside class="sidebar">
	<?php 
	do_action('awp_before_sidebar');
	dynamic_sidebar('left-sidebar'); 
	?>
</aside>

Rendering the menu

Now we can go back to functions.php, define a function hooked to awp_before_sidebar and its output will be displayed in the sidebar before the widgets. The function will use WordPress conditional tags to check whether or not we are currently showing a single post or page. And if so I will fetch our post meta. If the post meta was set we output the menu by calling wp_nav_menu() and providing the saved menu ID as its menu parameter.

add_action('awp_before_sidebar', function() {
	if (is_singular()) {
		global $post;

		$sidebar_menu = get_post_meta($post->ID, 'awp_sidebar_menu', true);
	}

	if (!empty($sidebar_menu)) {
		?><section class="widget awp-sidebar-menu">
			<?php wp_nav_menu(['menu' => $sidebar_menu]); ?>
		</section><?php
	}
});

You should adjust the HTML around the menu as to make it fit in with the rest of the content. In the code above I wrap the menu in the same HTML as all widgets in the sidebar is wrapped in so that the theme’s widget styling applies to our custom menu.

That’s it! Whenever you choose a menu in a post or page, the menu will be output above the sidebar when viewing that post or page.

We can take it one step further though. If you want children pages to show the same sidebar menu set in any of the parents, read on.

Allow child pages to inherit the parent’s menu

This additional feature makes sense if you have a lot of pages in a hierarchy, or a custom post type with hierarchy toggled on. It would be too cumbersome to edit every single child page and choose the same menu. In that case it’d be better to choose the menu on the parent page, and automatically let all subpages “inherit” that menu choice. If any subpage chooses another menu, that menu will be displayed instead of the “inherited” once.

Inside our function hooked to awp_before_sidebar, we’ll add a piece of code, inside the check if we are viewing a single post or page:

		...
		$sidebar_menu = get_post_meta($post->ID, 'awp_sidebar_menu', true);
		
		if (!empty($sidebar_menu)) {
			$parents = get_post_ancestors($post->ID);
			if (!empty($parents)) {
				// go step by step up the parents tree
				for ($i = 0; $i < count($parents); $i++) {
					$sidebar_menu = get_post_meta($post->ID, 'awp_sidebar_menu', true);
					if (!empty($sidebar_menu)) {
						break;
					}
				}
			}
		}
	}

	if (!empty($sidebar_menu)) {
		...

What the above code does if no menu was found on the current page is fetching all parents with get_post_ancestors(). This function returns an array of parent post IDs sorted by closest parent first. If the page has no parents (for example if it’s a post) an empty array is returned. And if there are any parents we loop through each parent one by one and checks if they have set our post meta. If one was found we break out of traversing the parents and $sidebar_menu will be set and the menu will be output later on with wp_nav_menu().

And that’s it for the “inheritance” functionality!

Leave a comment