Breadcrumb Schema JSON-LD for WordPress SEO

Breadcrumb schema (BreadcrumbList JSON-LD) tells search engines the hierarchical path to a page, enabling breadcrumb trails in Google Search results. WordPress generates breadcrumbs structurally but does not output JSON-LD schema by default — adding it requires a few lines of PHP that read the current page’s ancestors.

Problem: Google Search Console shows WordPress category, tag, and custom taxonomy archive pages in search results but without breadcrumb rich results — the HTML breadcrumb navigation is not marked up with schema that Google can parse.

Solution: Output a BreadcrumbList JSON-LD block in wp_head — build the item array from get_the_archive_title(), get_term_parents_list(), and get_permalink(), then encode and echo it. For single posts, walk up the category hierarchy. Validate with Google's Rich Results Test.

The examples below generate BreadcrumbList JSON-LD for posts (using categories), pages (using parent pages), WooCommerce products, and custom post types with custom taxonomies.

<?php
add_action( 'wp_head', 'myplugin_output_breadcrumb_schema' );

function myplugin_output_breadcrumb_schema(): void {
    if ( ! is_singular() && ! is_archive() && ! is_home() ) return;

    $items = myplugin_build_breadcrumb_items();
    if ( count( $items ) < 2 ) return; // no point showing just "Home"

    $list_items = [];
    foreach ( $items as $position => $item ) {
        $list_items[] = [
            '@type'    => 'ListItem',
            'position' => $position + 1,
            'name'     => $item['name'],
            'item'     => $item['url'],
        ];
    }

    $schema = [
        '@context'        => 'https://schema.org',
        '@type'           => 'BreadcrumbList',
        'itemListElement' => $list_items,
    ];

    echo '<script type="application/ld+json">'
        . wp_json_encode( $schema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES )
        . '</script>' . "
";
}

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

    if ( is_singular( 'post' ) ) {
        // Add primary category
        $cats = get_the_category();
        if ( ! empty( $cats ) ) {
            $cat = $cats[0];
            // Walk up category ancestors
            $ancestors = array_reverse( get_ancestors( $cat->term_id, 'category' ) );
            foreach ( $ancestors as $ancestor_id ) {
                $anc     = get_term( $ancestor_id, 'category' );
                $items[] = [ 'name' => $anc->name, 'url' => get_term_link( $anc ) ];
            }
            $items[] = [ 'name' => $cat->name, 'url' => get_term_link( $cat ) ];
        }
        $items[] = [ 'name' => get_the_title(), 'url' => get_permalink() ];

    } elseif ( is_singular( 'page' ) ) {
        // Walk up page parent chain
        $ancestors = array_reverse( get_ancestors( get_the_ID(), 'page' ) );
        foreach ( $ancestors as $ancestor_id ) {
            $items[] = [ 'name' => get_the_title( $ancestor_id ), 'url' => get_permalink( $ancestor_id ) ];
        }
        $items[] = [ 'name' => get_the_title(), 'url' => get_permalink() ];

    } elseif ( is_singular( 'product' ) ) {
        $terms = get_the_terms( get_the_ID(), 'product_cat' );
        if ( $terms && ! is_wp_error( $terms ) ) {
            $cat     = $terms[0];
            $items[] = [ 'name' => $cat->name, 'url' => get_term_link( $cat ) ];
        }
        $items[] = [ 'name' => get_the_title(), 'url' => get_permalink() ];
    }

    return $items;
}

Also output a visible breadcrumb trail using the same data:

<?php
// Output an HTML breadcrumb trail — uses the same data as the schema
function myplugin_render_breadcrumbs(): void {
    $items = myplugin_build_breadcrumb_items();
    if ( count( $items ) < 2 ) return;

    echo '<nav class="breadcrumbs" aria-label="' . esc_attr__( 'Breadcrumb', 'myplugin' ) . '">';
    echo '<ol itemscope itemtype="https://schema.org/BreadcrumbList">';

    $last = count( $items ) - 1;
    foreach ( $items as $pos => $item ) {
        $is_last = ( $pos === $last );
        echo '<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">';
        if ( ! $is_last ) {
            printf( '<a itemprop="item" href="%s"><span itemprop="name">%s</span></a>',
                esc_url( $item['url'] ), esc_html( $item['name'] ) );
        } else {
            printf( '<span itemprop="name" aria-current="page">%s</span>', esc_html( $item['name'] ) );
        }
        echo '<meta itemprop="position" content="' . ( $pos + 1 ) . '">';
        echo '</li>';
        if ( ! $is_last ) echo '<li aria-hidden="true">›</li>';
    }

    echo '</ol></nav>';
}

NOTE: Test your breadcrumb schema with Google's Rich Results Test. The JSON-LD and microdata outputs are equivalent — use JSON-LD (the <script> block) if you want to keep markup semantic and schema separate, which is Google's recommended approach.

Leave Comment

Your email address will not be published. Required fields are marked *