本文约 5,741 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:菜单做好了,前台一片空白
「外观 → 菜单」里建好了导航,勾了”主导航”位置,保存成功。前台却什么都不显示,或者显示的是一串默认页面列表。翻主题代码发现 header.php 里写的是 wp_nav_menu( array( 'theme_location' => 'menu-1' ) ),而你注册的位置叫 primary。
菜单这条链路有三段:注册位置、后台建菜单并指派到位置、前台按位置名调用。任意一段对不上,就会静默失败——不报错,只是不显示。
原理分析:三张表与一个映射关系
菜单数据存在数据库里,涉及三张表:
| 数据 | 存储位置 | 说明 |
|---|---|---|
| 菜单本身(容器) | wp_terms(taxonomy = nav_menu) |
一个菜单就是一个 term |
| 菜单项 | wp_posts(post_type = nav_menu_item) |
每项是一篇”菜单项文章” |
| 菜单与项的关联 | wp_term_relationships |
多对多 |
而”菜单位置”是另一回事,它不进数据库,而是运行时注册的。核心用两个概念区分:
① 位置(theme_location):主题用 register_nav_menus() 注册的"插槽",如 primary / footer
② 菜单(menu):用户在后台创建的菜单实体,如"主菜单""页脚菜单"
③ 指派关系:存在 theme_mods_{stylesheet} 这个 option 的 nav_menu_locations 数组里
wp_nav_menu() 的查找顺序是:
theme_location 有值 → 查 nav_menu_locations[位置] 有没有指派菜单
├─ 有 → 渲染该菜单
└─ 无 → 退回按 menu / menu_id 参数找 → 再退回 wp_page_menu() 输出页面列表
这解释了那个经典现象:位置没指派菜单时,前台会显示一份”所有页面列表”——这是 wp_page_menu() 的兜底输出,不是 bug。
保姆级实操
第 1 步:注册菜单位置
add_action( 'after_setup_theme', function () {
register_nav_menus( array(
'primary' => '主导航(页头)',
'footer' => '页脚导航',
'mobile' => '移动端抽屉菜单',
'social' => '社交链接(页脚右侧)',
) );
} );
数组键是位置标识(英文、小写、连字符),值是后台显示的名称(可中文)。位置标识一旦定下来就不要改,改名会让已指派的关系丢失——因为映射存在 theme_mods 里,键就是位置标识。
第 2 步:前台调用
// header.php
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => 'nav',
'container_class'=> 'main-navigation',
'container_id' => 'site-navigation',
'menu_class' => 'primary-menu',
'fallback_cb' => false, // 关掉页面列表兜底,便于排查
'depth' => 2, // 支持二级
'item_spacing' => 'discard', // 去掉 li 之间的换行空白,便于 inline-block 布局
) );
fallback_cb 默认值为 wp_page_menu,把它设为 false 后,”没指派菜单”就会什么都不输出,一眼能看出问题所在。
第 3 步:加判断与兜底
<?php if ( has_nav_menu( 'primary' ) ) : ?>
<?php
wp_nav_menu( array(
'theme_location' => 'primary',
'container' => 'nav',
'container_class'=> 'main-navigation',
'menu_class' => 'primary-menu',
'fallback_cb' => false,
) );
?>
<?php elseif ( current_user_can( 'manage_options' ) ) : ?>
<p class="menu-hint">
<a href="<?php echo esc_url( admin_url( 'nav-menus.php' ) ); ?>">
<?php esc_html_e( '请到后台指派主导航菜单。', 'mimangfei' ); ?>
</a>
</p>
<?php endif; ?>
只对管理员显示提示,普通访客看不到任何异常。has_nav_menu( 'primary' ) 内部查的就是 get_nav_menu_locations() 里该位置是否已指派且菜单存在。
第 4 步:自定义菜单 HTML 结构
默认的 <ul><li><a> 结构不满足需求(比如要加箭头图标、描述文字)时,用 Walker_Nav_Menu 子类:
class MMF_Walker_Nav_Menu extends Walker_Nav_Menu {
// 在每个 <li> 的开始处插入箭头,用于二级菜单
public function start_el( &$output, $item, $depth = 0, $args = null, $id = 0 ) {
$classes = empty( $item->classes ) ? array() : (array) $item->classes;
$has_child = in_array( 'menu-item-has-children', $classes, true );
$output .= sprintf(
'<li id="menu-item-%1$d" class="%2$s">',
esc_attr( $item->ID ),
esc_attr( implode( ' ', $classes ) )
);
$title = apply_filters( 'the_title', $item->title, $item->ID );
$output .= sprintf(
'<a href="%1$s"%2$s>%3$s</a>',
esc_url( $item->url ),
! empty( $item->target ) ? ' target="' . esc_attr( $item->target ) . '"' : '',
esc_html( $title )
);
if ( $has_child ) {
$output .= '<button class="submenu-toggle" aria-expanded="false"><span class="screen-reader-text">展开子菜单</span></button>';
}
}
}
调用时传 walker:
wp_nav_menu( array(
'theme_location' => 'primary',
'walker' => new MMF_Walker_Nav_Menu(),
'fallback_cb' => false,
) );
改造菜单项本身(加”高亮””新窗口打开”等字段)可另辟蹊径,参考主题选项面板实现方式对比里关于自定义器控件的做法。
第 5 步:用过滤器微调,不必写整个 Walker
只想在 <a> 上加属性时,用 nav_menu_link_attributes 就够了:
add_filter( 'nav_menu_link_attributes', function ( $atts, $item, $args ) {
if ( 'primary' === $args->theme_location ) {
$atts['class'] = 'nav-link';
if ( $item->target ) {
$atts['rel'] = 'noopener noreferrer';
}
}
return $atts;
}, 10, 3 );
// 给当前菜单项加 class(核心默认已有 current-menu-item)
add_filter( 'nav_menu_css_class', function ( $classes, $item, $args ) {
if ( in_array( 'current-menu-item', $classes, true ) ) {
$classes[] = 'is-active';
}
return $classes;
}, 10, 3 );
第 6 步:命令行核对指派关系
wp menu list
wp menu location list
wp menu location assign primary 5 # 把 ID 为 5 的菜单指派到 primary 位置
wp menu location list 的输出能直接确认”位置 → 菜单”是否对上,比在后台点来点去快。
常见踩坑
- 位置标识与调用时写的字符串不一致。 注册
primary,调用写menu-1,前台走兜底。 - 改了位置标识导致菜单丢失。 映射存在
theme_mods_{stylesheet}的nav_menu_locations里,键名一变,指派关系就断了。 register_nav_menus挂在init上太晚。 虽然多数情况仍能工作,但后台菜单页的”显示位置”可能不出现,统一挂after_setup_theme。- 子主题重复注册同名位置。 会覆盖父主题的注册,名称显示子主题的版本,通常无害但容易困惑。
- 菜单项太多导致后台保存失败。 这是 PHP 的
max_input_vars(默认 1000)限制,菜单项超过一定数量后保存被截断,需要在php.ini里调大到 3000 或用.htaccess的php_value max_input_vars 3000。表现为”勾了位置但保存后没生效”。 - 案例: 某站页脚菜单在中文站正常,英文站空白。查
wp menu location list发现两个语言的theme_mods是同一份(用了同一主题且未启用多语言插件的位置隔离),指派的是中文菜单 ID,英文站查不到对应项。最后改成在代码里按语言常量切换theme_location,才彻底解决。这类”多语言下的菜单指派”问题是菜单排查里的高频场景。 depth限制了层级但 CSS 没跟上。 勾了三级菜单,depth默认 0(无限),但主题 CSS 只写了二级的.sub-menu样式,三级菜单叠在一起显示。移动端抽屉菜单还需要一段初始化脚本,注入位置见wp_head 与 wp_footer 的作用。
总结
菜单排查的固定顺序是:wp menu location list 确认位置与菜单的指派关系 → 确认模板里 theme_location 的字符串与注册键名完全一致 → 把 fallback_cb 设为 false 排除兜底干扰。日常开发里,位置标识一旦上线就不要改;前台调用一律配 has_nav_menu() 做保护;只想改一两个属性时用 nav_menu_link_attributes 这类过滤器,需要改结构时才上 Walker。
