搜索全站
主题 17 分钟阅读

WordPress 面包屑与结构化数据:手动输出 BreadcrumbSchema

面包屑只做了视觉导航,搜索引擎读不懂。本文给出纯代码实现的面包屑函数、BreadcrumbList JSON-LD 输出、分类层级递归与验证方式,附与 SEO 插件共存的避坑清单。

U
UFO_PF 2025-05-31 · 阅读 17 分钟
Wo 主题
阅读提示
本文约 6,719 字,预计阅读 17 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:站点层级做得很清楚,搜索引擎看不见

文章页顶部那行「首页 › WordPress › 主题 › 本文」对人类读者很友好——一眼知道自己在哪里,也能点上一级。但对搜索引擎来说,如果这行只是普通的 <a> 链接,它并不能确定这是「面包屑」,只能当成普通的内部链接处理。加上结构化数据之后,Google 才会在搜索结果里把这条路径显示成层级化的面包屑,点击率通常有明显提升。

很多站长依赖 Yoast SEO、Rank Math 或 Breadcrumb NavXT 生成面包屑。这些方案没问题,但有两个常见困扰:一是插件的 HTML 结构与主题样式对不上,改起来要覆写模板;二是想在自定义文章类型、自定义分类法上加面包屑时,插件的映射配置比较绕。自己写一份只有几十行,可控性反而更好。本文给出完整实现,并说明如何与已有插件和平共处。

原理分析:面包屑数据与面包屑外观是两件事

面包屑实际上是两个独立的东西:

维度 视觉面包屑 结构化数据面包屑
面向 人类读者 搜索引擎
形式 HTML <nav> + 链接 JSON-LD <script type="application/ld+json">
要求 可点击、层级清晰 符合 schema.org BreadcrumbList 规范
一致性要求 两者必须表达同一条路径

schema.org 的 BreadcrumbList 结构是:一个 itemListElement 数组,每项是一个 ListItem,含 position(从 1 开始)、name、item(该层级的 URL)。最后一项(当前页面)按 Google 的规范可以省略 item,写上也不会报错。

数据生成时最难的是层级递归。分类是有父子关系的,文章页的面包屑要显示完整的分类链。WordPress 提供了 get_term_parents_list()(4.8 起),能直接返回带分隔符的父级链接串,省掉手写递归。对于自定义分类法同样适用,只要传对 $taxonomy。

输出位置的选择:wp_head 里输出 JSON-LD 最稳妥,Google 能稳定抓取。也可以放在页面底部,但不要放在需要 JS 执行后才插入的位置——部分抓取器不执行 JS。

保姆级实操

  1. 设计层级规则:首页 → 分类/归档 → 当前文章;页面类型走页面层级。
  2. 写面包屑数据函数:返回 name + url 的数组,便于同时服务 HTML 和 JSON-LD。
  3. 递归分类父级:用 get_term_parents_list() 或自己递归 get_term()。
  4. 输出 HTML:<nav aria-label="面包屑"> + <ol> 列表,语义化更好。
  5. 输出 JSON-LD:在 wp_head 里用 wp_json_encode() 输出,注意转义。
  6. 验证:用 Google 富媒体搜索结果测试工具或 schema.org 校验器检查。
  7. 与插件共存:已装插件时关掉自己的输出,避免两份 JSON-LD。
<?php
// 子主题 functions.php:生成面包屑数据(同时供 HTML 与 JSON-LD 使用)
function mimangfei_breadcrumb_items() {
    $items = array();

    $items[] = array(
        'name' => get_bloginfo( 'name' ),
        'url'  => home_url( '/' ),
    );

    if ( is_front_page() ) {
        return $items;
    }

    if ( is_archive() || is_single() ) {
        $tax = is_single() ? 'category' : get_queried_object()->taxonomy;
        $term = null;

        if ( is_single() ) {
            $terms = get_the_terms( get_the_ID(), 'category' );
            if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
                $term = $terms[0];
            }
        } else {
            $term = get_queried_object();
        }

        if ( $term && ! is_wp_error( $term ) ) {
            // 先补齐所有父级分类
            $parents = get_term_parents_list(
                $term->term_id,
                $tax,
                array( 'separator' => '', 'link' => false, 'format' => 'slug' )
            );
            $slugs = array_filter( explode( '/', trim( (string) $parents, '/' ) ) );
            $slugs[] = $term->slug;

            foreach ( $slugs as $slug ) {
                $t = get_term_by( 'slug', $slug, $tax );
                if ( $t && ! is_wp_error( $t ) ) {
                    $items[] = array(
                        'name' => $t->name,
                        'url'  => get_term_link( $t ),
                    );
                }
            }
        }
    }

    if ( is_single() ) {
        $items[] = array(
            'name' => get_the_title(),
            'url'  => get_permalink(),
        );
    }

    if ( is_page() ) {
        $ancestors = array_reverse( get_post_ancestors( get_the_ID() ) );
        foreach ( $ancestors as $post_id ) {
            $items[] = array(
                'name' => get_the_title( $post_id ),
                'url'  => get_permalink( $post_id ),
            );
        }
        $items[] = array(
            'name' => get_the_title(),
            'url'  => get_permalink(),
        );
    }

    return $items;
}
<?php
// 输出 JSON-LD:最后一项省略 item,符合 Google 规范
add_action( 'wp_head', 'mimangfei_breadcrumb_json_ld', 20 );
function mimangfei_breadcrumb_json_ld() {
    // 已装 SEO 插件时直接让路,避免两份数据冲突
    if ( defined( 'WPSEO_VERSION' ) || class_exists( 'RankMath' ) ) {
        return;
    }

    $items = mimangfei_breadcrumb_items();
    if ( count( $items ) < 2 ) {
        return;
    }

    $list = array();
    foreach ( $items as $i => $item ) {
        $element = array(
            '@type'    => 'ListItem',
            'position' => $i + 1,
            'name'     => $item['name'],
        );
        // 最后一项只给 name,不给 item
        if ( $i < count( $items ) - 1 ) {
            $element['item'] = $item['url'];
        }
        $list[] = $element;
    }

    $data = array(
        '@context'        => 'https://schema.org',
        '@type'           => 'BreadcrumbList',
        'itemListElement' => $list,
    );

    echo '<script type="application/ld+json">'
       . wp_json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES )
       . '</script>' . "\n";
}
<?php
// 视觉面包屑:语义化列表 + 微数据无关,纯 HTML
function mimangfei_breadcrumb_html() {
    $items = mimangfei_breadcrumb_items();
    if ( count( $items ) < 2 ) {
        return '';
    }

    $html = '<nav class="breadcrumb" aria-label="面包屑"><ol>';
    $last = count( $items ) - 1;
    foreach ( $items as $i => $item ) {
        if ( $i === $last ) {
            $html .= '<li aria-current="page">' . esc_html( $item['name'] ) . '</li>';
        } else {
            $html .= '<li><a href="' . esc_url( $item['url'] ) . '">'
                   . esc_html( $item['name'] ) . '</a></li>';
        }
    }
    $html .= '</ol></nav>';
    return $html;
}
.breadcrumb ol {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    list-style: none;
    margin: 0 0 1rem;
    padding: 0;
    font-size: 0.85rem;
    color: var( --muted, #656d76 );
}

.breadcrumb li + li::before {
    content: "/";
    margin-right: 6px;
    color: var( --border, #d0d7de );
}

常见踩坑

  1. JSON-LD 里 URL 用了相对路径:item 必须是绝对 URL,否则校验器报错、Google 无法识别。
  2. position 从 0 开始:schema.org 的 position 从 1 开始,从 0 起会被判为无效。
  3. 案例:某站面包屑在 Google 搜索结果里一直不显示,校验器却说「有效」。排查发现他们输出了两份 BreadcrumbList——主题一份、SEO 插件一份,且两家的层级不一致(主题按文章所属的第一个分类,插件按主分类)。Google 遇到同类型多份数据时可能全部忽略。最后关掉主题那份,统一由插件输出,两周后搜索结果里正常显示了面包屑。教训:同类型结构化数据只保留一份。
  4. get_term_parents_list() 的 format 参数用错:传 'name' 时返回的是名称而非 slug,再拿去 get_term_by( 'slug' ) 会查不到。要么统一用 slug,要么直接用 link => true 拿现成的链接串。
  5. 文章属于多个分类时取哪个不确定:get_the_terms() 返回的顺序不保证稳定,面包屑可能忽而走 A 分类忽而走 B 分类。指定主分类(Yoast 的 primary_category 或自己加个字段)更可控。
  6. 中文没有做转义:wp_json_encode() 默认会把中文转成 \uXXXX,虽然有效但可读性差。加上 JSON_UNESCAPED_UNICODE 更直观。
  7. wp_head 里输出时没判断首页:首页也会生成只有一项的面包屑,属于冗余。加 count() < 2 的判断。
  8. 面包屑出现在 404 页:404 页的层级无从谈起,输出会出错。用 ! is_404() 提前 return,404 页的做法另见 主题 404 页面的自定义设计。

总结

面包屑的判断标准是:视觉路径与结构化数据必须表达同一条路径,且全站只输出一份 JSON-LD。行动清单:写一个同时服务 HTML 与 JSON-LD 的数据函数 → 用 get_term_parents_list() 递归分类父级 → wp_head 输出 JSON-LD,position 从 1 开始、末项省略 item → 已装 SEO 插件时主动让路 → 用富媒体测试工具验证 → 在 Google Search Console 的「面包屑」报告里观察几周。做完这项,可以接着处理 主题分页与 SEO:rel=next/prev 的现状,把列表页的结构化表达一并理顺。

U
UFO_PF作者

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

留下你的想法

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