搜索全站
插件 14 分钟阅读

WordPress 目录(TOC)自动生成:插件方案与无插件实现完整指南

长文没人看,多半是缺了目录。本文对比 Easy Table of Contents、LuckyWP Table of Contents 与纯代码方案,讲清锚点生成规则、层级过滤、样式控制与缓存冲突处理,附可直接用的 PHP 代码。

U
UFO_PF 2017-01-28 · 阅读 14 分钟
Wo 插件
阅读提示
本文约 5,570 字,预计阅读 14 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:一篇文章 6000 字,读者找不到第二段在哪

技术文章最常见的问题不是写得浅,而是写得太长又没有路标。读者从搜索引擎点进来,关心的是某个具体报错的解法,结果页面往上滑三屏还在铺垫背景,于是直接关闭——跳出率拉满,页面停留时长被压到十几秒。国内不少教程站把长文拆成一页一页做分页,效果反而更差:多页阅读会稀释正文权重,第二页之后的页面基本不被收录。

在文章顶部自动插入一块目录(Table of Contents,简称 TOC),是成本最低的补救手段。它有三重收益:让读者一秒判断这篇值不值得读;Google 会把带锚点跳转的目录识别为”跳转链接(Jump links)”,在搜索结果里展示小标题级别的二级入口;同时目录里的 <h2>/<h3> 文本还能强化页面的语义结构。实现方式分两派:装插件,或者用二十来行 PHP 自己解析标题。下面把两条路都讲透。

原理分析:目录 = 解析标题 + 生成锚点 + 前端跳转

无论用什么方案,本质流程都一样,差别只在于谁来做和什么时候做。

环节 插件方案 代码方案
抓取时机 the_content 过滤器,优先级 9~11 the_content 过滤器,自定义优先级
锚点来源 重写标题的 id 属性(slugify) 用 sanitize_title() 生成 id
层级控制 后台可选 h2~h6 组合 正则匹配指定标签数组
样式 自带 CSS + 主题 override 完全自定义 CSS
首屏性能 多一份 CSS/JS 请求(约 3–8 KB,可内联) 零额外请求

关键点在锚点:WordPress 编辑器输出的 <h2> 默认不带 id,所以必须先给每个标题补 id 属性,目录才能跳。这一步做不好,就会出现”点目录没反应”的经典故障。

第二个要点是 the_content 的优先级。很多页面构建器和表格插件也在 the_content 上跑钩子,如果 TOC 的优先级设得太低(比如 20),解析到的可能是别人处理之后的 HTML;设得太高(比如 4),又可能在 wpautop 之前解析,导致标题标签判断失误。稳妥区间是 9 到 11。

延伸阅读:WordPress 短代码开发与区块化取舍

保姆级实操

步骤 1:插件方案选型(不想写代码走这条)

推荐 Easy Table of Contents(作者 Magazine3,wordpress.org 插件数安装量超过 30 万)。安装后进入「设置 → Table of Contents」,核心选项如下:

  • Position:选 Before first heading(插到第一个标题之前),不要选 After first paragraph,否则段落会被目录硬生生截断。
  • Headings:勾 h2 + h3 即可,勾满 h2~h6 会让三层以上的目录缩进失控。
  • Smooth Scroll Offset:如果你的主题是 sticky header,这里填 header 高度(常见 60~90),否则点击后标题会被导航栏盖住。
  • Show when:设为 post 即可,别对 page 全局生效,落地页插目录会破坏转化路径。

LuckyWP Table of Contents 是另一个免费选择,它的优势是自带”appearence → container width”精细控制,适合侧边栏宽度受限的双栏主题。两者都遵循同一个坑:插件目录默认对所有文章类型生效,需要用插件后台的 Post Types 选项收窄。

步骤 2:关闭某一篇的目录

不需要在每篇文章都显示目录。插件支持两类关闭方式:文章编辑页右侧的 meta 框(Easy TOC 提供「Disable the automatic generation of the table of contents」复选框),或者用一行代码在主题层全局禁用某篇文章:

// functions.php:ID 为 1234 的文章不生成目录
add_filter( 'ez_toc_maybe_apply_the_content_filter', function( $apply ) {
    if ( is_singular( 'post' ) && get_the_ID() === 1234 ) {
        return false;
    }
    return $apply;
} );

步骤 3:无插件方案——完整可运行代码

把下面这段丢进主题的 functions.php,它会在正文第一级标题之前插入目录,并给每个 <h2>/<h3> 补上 id。代码用 DOMDocument 做解析,避免正则 Nested parse 出错:

function mft_build_toc( $content ) {
    if ( ! is_singular( 'post' ) || ! in_the_loop() || ! is_main_query() ) {
        return $content;
    }
    // allow_url 相关兼容:用 UTF-8 包裹,避免中文乱码
    $html = '<?xml encoding="utf-8" ?>' . $content;
    $dom  = new DOMDocument();
    libxml_use_internal_errors( true );
    $dom->loadHTML( $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
    libxml_clear_errors();

    $levels  = array( '2', '3' );
    $nodes   = array();
    foreach ( $levels as $lv ) {
        foreach ( $dom->getElementsByTagName( 'h' . $lv ) as $node ) {
            $nodes[] = array( 'lv' => (int) $lv, 'node' => $node );
        }
    }
    if ( count( $nodes ) < 3 ) {
        return $content; // 标题太少就不插目录
    }

    $toc = '<nav class="mf-toc" aria-label="文章目录"><p class="mf-toc-title">目录</p><ol>';
    foreach ( $nodes as $item ) {
        $text = trim( $item['node']->textContent );
        $id   = 'toc-' . sanitize_title( $text ) . '-' . md5( $text );
        $item['node']->setAttribute( 'id', $id );
        $toc .= sprintf(
            '<li class="mf-toc-item mf-toc-lv%d"><a href="#%s">%s</a></li>',
            $item['lv'], esc_attr( $id ), esc_html( $text )
        );
    }
    $toc .= '</ol></nav>';

    $out = $dom->saveHTML();
    $out = preg_replace( '#<\?xml encoding="utf-8" \?>#', '', $out );

    // 插到第一个 h2 之前
    $first_h2 = $nodes[0]['node']->ownerDocument->saveHTML( $nodes[0]['node'] );
    return str_replace( $first_h2, $toc . $first_h2, $out );
}
add_filter( 'the_content', 'mft_build_toc', 11 );

步骤 4:配套 CSS

.mf-toc {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    padding: 14px 18px;
    margin: 0 0 24px;
    background: #f9fafb;
}
.mf-toc-title { margin: 0 0 8px; font-weight: 600; font-size: 15px; }
.mf-toc ol { margin: 0; padding-left: 0; list-style: none; }
.mf-toc-lv3 > a { padding-left: 16px; font-size: 13px; color: #4b5563; }
.mf-toc a { display: block; padding: 4px 0; line-height: 1.7; text-decoration: none; }
.mf-toc a:hover { color: #c0392b; }
/* 解决 sticky header 遮挡:配合 html 的 scroll-padding-top */
html { scroll-padding-top: 90px; }

步骤 5:验证

随便打开一篇文章,右键查看源代码,确认 <h2> 标签带了 id="toc-xxxx";点目录任意一项,URL 末尾应出现 #toc-...;用浏览器开发者工具切到手机视图,确认目录在小屏没有溢出容器。

常见踩坑

  1. 点击目录无反应:八成是 id 没生成。原因通常是主题输出 <h2> 时本身已被其他钩子改写,或者 sanitize_title() 对纯中文返回空串(旧版核心在某些字符集下会这样),此时必须靠 md5 兜底——上面代码里已经加了。
  2. 目录出现在 RSS 里:the_content 会同时作用于 the_excerpt_rss 之外的输出。加 if ( is_feed() ) return $content; 把 Feed 排除掉。
  3. 案例:某站长把 Easy TOC 的 Position 设成 After first paragraph,同时开启了 WP Rocket 的延迟 JS。结果目录在前 500 篇文章里全部压在图片下方,搜索引擎快照里满屏都是”1. 引言 2. 原理 3. 实操”,摘要被目录污染。改回 Before first heading、并在 PageSpeed 把 defer JS 对该页面的 toc.js 加白名单之后才恢复。
  4. 和表格插件打架:TablePress 用 the_content 输出表格,优先级冲突会导致目录出现在表格单元格里。把 TOC 钩子优先级调到 11,并把 TablePress 的输出放到 wpautop 之后。
  5. 中文标点导致锚点失效:标题里含 ?、, 时,urlencode 之后锚点可能不再匹配。用 sanitize_title() + hash 的方式可以彻底规避。
  6. sticky header 遮住标题:光靠 scroll-padding-top 有时不够,部分主题给 body 加了 overflow-x: hidden,会让基于 html 的滚动属性失效,这种情况要在主题 CSS 里补 body { scroll-padding-top: 90px; }。
  7. 目录被算进正文相关性:如果标题超过 40 个(比如长列表文章),目录文本量会很大。设置”少于 3 个标题不显示、超过 35 个标题折叠”,并给 <nav> 加 aria-label 让辅助技术正确识别。

总结

判断标准很简单:内容层级稳定、编辑团队多人协作用插件,Easy TOC 胜在选项直观、能按文章单独关闭;追求极致轻量、文章结构固定用代码方案,二十多行即可零依赖实现。无论哪种,落地后请务必检查三件事:标题是否真的带上了 id、is_feed() 是否排除、sticky header 是否被 scroll-padding-top 补偿。至于悬浮式侧边目录,只在宽度 ≥1200px 的阅读型站点上有收益,营销落地页不要加。

相关配置:WordPress 站点缓存排除规则设置

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。