I once needed to place a menu in the middle of a post, somewhere the theme had no widget area at all. Editing the template would have worked, but then every time the client wanted to move it they would have to come back to a developer. The tidier solution is to wrap the menu in a shortcode.
The basic shortcode
Add this to the theme’s functions.php, ideally in a child theme:
<?php
add_shortcode( 'footer-menu', 'footer_menu_shortcode' );
function footer_menu_shortcode( $atts, $content = null ) {
$atts = shortcode_atts(
array(
'name' => null,
'class' => 'footer-menu',
),
$atts,
'footer-menu'
);
return wp_nav_menu(
array(
'menu' => $atts['name'],
'menu_class' => $atts['class'],
'echo' => false,
)
);
}
Then drop it wherever you want it:
[footer-menu name="custom-menu"]
Where custom-menu is the name of a menu you created under Appearance > Menus.
Three details that decide whether it works
'echo' => false is mandatory. By default wp_nav_menu() prints the menu directly. Forget this parameter and the menu jumps to the top of the page instead of appearing where you placed the shortcode, because a shortcode callback has to return a string, never echo one.
Always return, never echo. That is the general rule for every WordPress shortcode, not just this one.
Do not use extract(). My first version used extract( shortcode_atts( ... ) ) because a lot of tutorials did it that way. That function creates variables dynamically from an array, which leaves the reader wondering where $name came from, and it can overwrite existing variables. The WordPress documentation itself recommends against it. Accessing $atts['name'] directly, as above, is both clearer and safer.
The third argument to shortcode_atts
The 'footer-menu' string passed as the third argument is not decoration. It activates the shortcode_atts_footer-menu filter, which lets another plugin change your defaults without editing your code. A small habit, but worth having.
Handling a menu that does not exist
If someone types the menu name wrong, wp_nav_menu() falls back to printing the default page list, which produces a very confusing result. Block that behaviour:
return wp_nav_menu(
array(
'menu' => $atts['name'],
'menu_class' => $atts['class'],
'echo' => false,
'fallback_cb' => '__return_empty_string',
'container' => 'nav',
)
);
With fallback_cb set like that, a wrong menu name renders nothing at all instead of rendering the wrong thing.
Allowing shortcodes in widgets
Text widgets in older WordPress versions do not process shortcodes on their own. If you need that:
add_filter( 'widget_text', 'do_shortcode' );
Reusing it elsewhere
The shortcode is named footer-menu, but it inserts any menu, not just a footer one. If you want a more neutral name, change the first argument of add_shortcode():
add_shortcode( 'wp-menu', 'footer_menu_shortcode' );
Then use [wp-menu name="main-menu"]. You can even register both names pointing at the same function, so old posts using the old name keep working.