Typecho导航菜单添加分类教程(含避坑指南)

Typecho 默认主题的导航菜单只显示独立页面而不显示分类。本文给出 Widget_Metas_Category_List 的最小可用代码、页面与分类合并写法、多级下拉、ignore 黑名单、ARIA 与缓存优化的完整方案。

更新 24 分钟阅读 3,306 阅读

保哥维护 Typecho 站点这些年,被人问得最多的就是导航条的事。默认主题里 header.php 只渲染独立页面(Page),分类(Category)压根不出现在顶部菜单。这件事在官方文档里其实有暗示,但没有一个完整的、能直接复制到生产站的范例。本文把我自己常用的几段代码、踩过的坑、以及和搜索引擎抓取相关的小细节都摊开来写一遍,方便后来人少走弯路。

为什么默认主题只显示独立页面

如果你打开 Typecho 自带 default 主题的 header.php,会看到类似 Widget_Contents_Page_List 的组件调用。这个组件查询的是 type='page' 的内容,所以分类天然就不会出现。Typecho 的设计哲学是把内容(contents)和分类(metas)分两张表存:typecho_contents 放文章和页面,typecho_metas 放分类和标签。两边通过 typecho_relationships 表多对多关联。所以菜单要同时显示页面和分类,就需要分别调用两个 Widget。

保哥早年第一次改这个的时候,犯过一个低级错误:把分类列表 Widget 写在 Widget_Contents_Page_List 的 while 循环里面,结果分类菜单只在第一个页面循环里出现一次,逻辑全乱。所以请记住,两个 Widget 是平级关系,foreach/while 各自闭合,互不嵌套。

另一个常被问的版本差异:Typecho 1.0、1.1、1.2、1.3 这四个版本里 Widget_Metas_Category_List 的方法签名几乎没变,只在 1.3 里 toString 部分加了新的命名空间形式。如果你跟着本文 1.3 的写法做,往老版本上回退也能用。

最小可用代码:把分类塞进顶部导航

下面这段是我现在线上用的版本,直接放在 header.php<ul class="nav-menu"> 内部即可:

<?php $this->widget('Widget_Metas_Category_List')->to($category); ?>
<?php while ($category->next()): ?>
  <li<?php if ($this->is('category', $category->slug)): ?> class="current"<?php endif; ?>>
    <a href="<?php $category->permalink(); ?>"><?php $category->name(); ?></a>
  </li>
<?php endwhile; ?>

几个要点说明:

  • Widget_Metas_Category_List 默认只取顶级分类,并按照后台"管理 → 分类"里调整的排序输出。
  • $this->is('category', $category->slug) 用来给当前所在分类加 current 类名,方便 CSS 高亮。
  • permalink() 输出的链接受"设置 → 永久链接"影响,建议先把伪静态打开,否则菜单里全是 ?cid= 这种丑链接,对 SEO 不利。

如果你的主题菜单是用 <nav> + <ul> 写的,记得包一层 <ul>,让上面的 <li> 有合法父元素,否则 HTML 校验会报错,部分浏览器渲染也会出问题。

同时显示页面和分类的合并写法

很多博主希望导航条上既有"关于我""留言板"这种独立页面,也有"Typecho 教程""MySQL"这种分类。我推荐的做法是页面在前、分类在后,这样符合大多数用户的浏览习惯,也方便面包屑(breadcrumb)的层级设计:

<ul class="nav-menu">
  <li<?php if ($this->is('index')): ?> class="current"<?php endif; ?>>
    <a href="<?php $this->options->siteUrl(); ?>">首页</a>
  </li>

  <?php $this->widget('Widget_Contents_Page_List')->to($pages); ?>
  <?php while ($pages->next()): ?>
    <li<?php if ($this->is('page', $pages->slug)): ?> class="current"<?php endif; ?>>
      <a href="<?php $pages->permalink(); ?>"><?php $pages->title(); ?></a>
    </li>
  <?php endwhile; ?>

  <?php $this->widget('Widget_Metas_Category_List')->to($category); ?>
  <?php while ($category->next()): ?>
    <li<?php if ($this->is('category', $category->slug)): ?> class="current"<?php endif; ?>>
      <a href="<?php $category->permalink(); ?>"><?php $category->name(); ?></a>
    </li>
  <?php endwhile; ?>
</ul>

这段代码塞进任何主题都能跑,前提是你已经在后台建好了页面和分类。我自己 zhangwenbao.com 的导航就是这个结构,已经稳定运行八年没出过问题。

多级分类与下拉菜单的处理

保哥个人不太喜欢三级以上的分类层级,原因是导航条一旦做下拉,移动端体验立刻变差。但如果你的站点必须有父子分类,可以这样写:

<?php $this->widget('Widget_Metas_Category_List')->to($category); ?>
<?php while ($category->next()): ?>
  <?php if ($category->parent == 0): // 只输出顶级 ?>
    <li class="has-children">
      <a href="<?php $category->permalink(); ?>"><?php $category->name(); ?></a>
      <?php if (!empty($category->children)): ?>
        <ul class="sub-menu">
          <?php foreach ($category->children as $child): ?>
            <li><a href="<?php echo $child['permalink']; ?>"><?php echo $child['name']; ?></a></li>
          <?php endforeach; ?>
        </ul>
      <?php endif; ?>
    </li>
  <?php endif; ?>
<?php endwhile; ?>

注意 $category->children 是一个普通 PHP 数组,不是 Widget 对象,所以访问字段用 ['permalink'] 而不是 ->permalink(),这是新手最容易写错的地方。

下拉菜单的 CSS 我习惯用纯 hover + transform 实现,不依赖 JS,例如:

.nav-menu .has-children .sub-menu {
  display: none;
  position: absolute;
  background: #fff;
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.nav-menu .has-children:hover > .sub-menu {
  display: block;
}

如果你想做点花哨的过渡效果,把 display 换成 opacity + visibility + transform 组合,鼠标滑过有渐隐渐现,移动端落到 onclick 时也不会突然弹出来吓人。

排除某些分类不在菜单显示

实际运营中有些分类是"归档用"而不是"展示用",比如我会把"站务公告"这种关掉对外曝光。Widget_Metas_Category_List 支持传 ignore 参数:

<?php $this->widget('Widget_Metas_Category_List@nav', 'ignore=12,15')->to($category); ?>

这里的 @nav 是 Widget 实例别名,避免和模板里其他地方重复实例化时缓存冲突。ignore=12,15 表示忽略 mid 等于 12 和 15 的分类。mid 在后台"管理 → 分类 → 编辑"页面的 URL 里能看到。

如果反过来你只想显示某几个分类,就用 mid 参数白名单:

<?php $this->widget('Widget_Metas_Category_List@nav', 'mid=3,7,9')->to($category); ?>

白名单和黑名单不要同时传,传了系统会以白名单优先。

带图标的分类菜单

不少博客主题会在分类名前加一个小图标。Typecho 里给分类加图标的标准做法是用分类的 description 字段存一个 icon 类名(比如 fa-folder),渲染时读出来:

<?php while ($category->next()): ?>
  <li>
    <a href="<?php $category->permalink(); ?>">
      <?php $iconClass = trim($category->description) ?: 'fa-folder'; ?>
      <i class="fa <?php echo htmlspecialchars($iconClass); ?>"></i>
      <?php $category->name(); ?>
    </a>
  </li>
<?php endwhile; ?>

htmlspecialchars 包一下,避免有人把 description 写成 HTML 注入到导航条里。

顺手补一下 SEO 与无障碍细节

保哥这两年做主题 v2 改版,把导航菜单的语义化又抠了一遍,分享几个搜索引擎和读屏软件都喜欢的写法:

  • 外层用 <nav aria-label="主导航"> 包裹,让屏幕阅读器知道这是导航区,不是普通列表。
  • 当前页对应的菜单项加 aria-current="page",比单纯一个 CSS class 更被 Google 重视。
  • 分类链接的锚文本就用分类名,不要画蛇添足写成"点击查看 XX 分类"。简短、直接、含关键词,是导航锚文本的黄金法则。
  • 菜单不要过长。十个以内是经验阈值,超过就该考虑分组或者下拉,否则移动端会换行成两排,影响 LCP 指标。
  • 给当前菜单项加 itemprop="url" + Schema.org BreadcrumbList 微数据,能让 Google 在搜索结果里展示带导航路径的富摘要。

带 ARIA 属性的完整版:

<nav aria-label="主导航">
  <ul class="nav-menu">
    <?php $this->widget('Widget_Metas_Category_List')->to($category); ?>
    <?php while ($category->next()): ?>
      <li>
        <a href="<?php $category->permalink(); ?>"
           <?php if ($this->is('category', $category->slug)): ?>aria-current="page"<?php endif; ?>>
          <?php $category->name(); ?>
        </a>
      </li>
    <?php endwhile; ?>
  </ul>
</nav>

给菜单做条数缓存

如果你的站点分类比较多(几十个以上),每次请求都重新跑一遍 Widget_Metas_Category_List 会有点浪费。Typecho 的 Widget 自带一层进程内缓存,但不跨请求。要做跨请求缓存可以用主题的 functions.php:

function baoge_cached_categories() {
  $cacheFile = __TYPECHO_ROOT_DIR__ . '/usr/cache/nav_categories.html';
  if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 600)) {
    return file_get_contents($cacheFile);
  }
  ob_start();
  // 此处放渲染分类菜单的代码
  $html = ob_get_clean();
  @file_put_contents($cacheFile, $html);
  return $html;
}

10 分钟刷新一次基本够用。后台改了分类怎么办?在分类保存的钩子里把缓存文件 unlink 一次即可。具体钩子是 Widget_Abstract_Metas 的 finishInsert / finishUpdate。

改完不生效的常见原因

这一节是我后台答疑回复最多的内容,干脆罗列出来:

  1. 改错了文件。Typecho 主题在 usr/themes/<主题名>/header.php,不是根目录的那个 header。如果你在 default 主题里改完没生效,看看后台"控制台 → 外观"里启用的到底是哪个主题。
  2. 缓存没清。装了 typecho-fans 的缓存插件、或者前面挂了 Cloudflare/Nginx FastCGI 缓存,记得清一遍。命令行清 Nginx 缓存:find /var/cache/nginx -type f -delete && systemctl reload nginx
  3. Widget 名拼错Widget_Metas_Category_List 不是 Widget_Meta_Category_List,少个 s 就 500。
  4. 分类没文章Widget_Metas_Category_List 默认会过滤掉空分类。如果你想空分类也显示,传 hasContents=false$this->widget('Widget_Metas_Category_List', 'hasContents=false')->to($category);
  5. 多次实例化未加 @alias。同一模板里多次调用 $this->widget('Widget_Metas_Category_List'),第二次会拿到第一次遍历完的指针位置,看起来"分类菜单只剩最后一项"。给两次调用分别加 @nav1@nav2 别名即可。
  6. permalink() 返回带 query string 的丑链。后台"设置 → 永久链接"没开启重写规则。开启后看 nginx 是否拒绝识别 .htaccess(要在 nginx 主域配置里加 try_files 规则)。

常见问题解答

为什么我加了代码以后菜单没变化,但查看源码确实有 li 输出了?

大概率是 CSS 的问题。原主题的 .nav-menu li 可能依赖某个固定的父元素 class,你新加的 <li> 缺少这个 class 导致显示为 display:none。打开浏览器 DevTools 看 computed style 就能定位。另一个高频原因是父元素被 flex 折叠到溢出区域,宽度不够时整行被截掉。

分类链接想用自定义字段里的别名怎么办?

后台"管理 → 分类 → 编辑"里有"缩略名"字段,那就是 slug,permalink 直接用。如果想强行换链接,可以在主题 functions.php 里 hook Widget_Metas_Category_List 的过滤器,但保哥不建议这么搞,伤 SEO 又难维护。要做强制跳转可以走 nginx 层 rewrite 规则。

Widget_Metas_Category_List 的输出顺序怎么调整?

后台"管理 → 分类"页面顶部有"调整顺序"按钮,拖动以后保存,前台菜单顺序立即跟着变。不需要在代码里写 order=。Typecho 的分类排序字段是 typecho_metas.order,调整顺序就是改这个字段。

能否做出当前文章所在分类高亮的效果?

可以。在文章页 $this->is('post') 时,遍历 $this->categories 拿到当前文章的分类 slug 列表,比对菜单里的分类 slug 即可。代码可以参考我 GitHub 上的 zhangwenbao-v2 主题源码,里面有完整实现。

分类下又分子分类,怎么在菜单里渲染三级?

Widget_Metas_Category_List 的 $category->children 数组本身是树形结构,每个 child 又有 children 字段。递归一个 PHP 函数处理即可:函数接收一个分类节点和深度参数,输出 li 并对它的 children 数组继续递归。注意 CSS 上要给三级菜单单独一套样式,否则会和二级混在一起。

能不能把 Typecho 分类菜单输出成 JSON 给前端 JS 渲染?

可以。在主题里写一个独立的入口文件比如 theme/api/categories.php,引入 Typecho 内核后实例化 Widget_Metas_Category_List 遍历,把每个分类的 mid、name、slug、permalink、parent 收集成数组,最后 json_encode 输出,前端 fetch 这个接口即可。这种做法适合做单页应用风格的导航。

菜单在移动端折叠时怎么处理?

纯 CSS 用 display: none + 媒体查询 + 一个 checkbox 触发的 hack 就能做出来无 JS 的汉堡菜单。如果要更高级的动画,引入一个轻量级 JS 即可,几十行就能搞定。注意 aria-expanded 属性的同步,确保读屏软件能识别菜单展开状态。

实战:从 0 到 1 给 Typecho 站点装上分类导航的最短路径

给完全新手用的步骤清单,按顺序做完就行:

  1. 登录 Typecho 后台,"管理 → 分类",确认你已经有 3 个以上分类,没有就建一下,每个分类填好名称和缩略名(slug)。
  2. "控制台 → 外观"看清楚现在启用的主题名,比如是 default
  3. 用 FTP 或 SSH 进 /usr/themes/default/ 目录,找到 header.php。先备份一份命名为 header.php.bak
  4. 打开 header.php,找到主导航的 <ul>。把本文"最小可用代码"那段贴进去,保存。
  5. 清缓存:宝塔面板 → 网站 → 设置 → 性能调整 里点一次"清空 OPcache"。挂了 Cloudflare 的也清一下边缘缓存。
  6. 无痕模式打开站点首页,看顶部导航是不是出现了所有分类。出现了就成。
  7. 有问题回去看"改完不生效的常见原因"那一节,按 6 条排查。

小结

把分类塞进 Typecho 导航菜单本身只是一行 Widget 调用的事,但要做得好看、对 SEO 友好、对无障碍友好,还需要顺手处理排序、ignore、ARIA、缓存这些细节。保哥的建议是:先用最小可用代码跑通,再按本文后半段的要点一项一项加固。这样改出来的导航菜单,随便扛个十年都不会塌。

分享到
标签
版权声明

本文标题:《Typecho导航菜单添加分类教程(含避坑指南)》

本文链接:https://zhangwenbao.com/typecho-add-category-to-navigation-menu.html

版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0

继续阅读
1 条评论
  1. iremex 的头像
    #1 中国安徽省合肥市电信

    您好,使用这个代码在header.php中,有二级分类,结果一级二级分类都出来了,并且错位了,怎么解决呢?

发表评论
分享到微信 或在下方手动填写
支持 Ctrl + Enter 提交