DEV3 Technologies home

Breadcrumbs with BreadcrumbList schema

Build a breadcrumb trail for pages, posts, archives and search, render it as an accessible list and add matching JSON-LD.

WordPressNavigationbreadcrumbsschema-orgseojson-ld

Code

<?php
/**
 * Breadcrumb trail + BreadcrumbList JSON-LD.
 *
 * @package Acme_Theme
 */

defined( 'ABSPATH' ) || exit;

/** @return array<int, array{label: string, url: string}> */
function acme_breadcrumb_trail(): array {
	$trail = array( array( 'label' => __( 'Home', 'acme-theme' ), 'url' => home_url( '/' ) ) );

	if ( is_singular() && ! is_front_page() ) {
		$post = get_queried_object();
		if ( 'post' === $post->post_type ) {
			$cats = get_the_category( $post->ID );
			if ( $cats ) {
				$trail[] = array( 'label' => $cats[0]->name, 'url' => get_category_link( $cats[0] ) );
			}
		}
		foreach ( array_reverse( get_post_ancestors( $post ) ) as $parent_id ) {
			$trail[] = array( 'label' => get_the_title( $parent_id ), 'url' => get_permalink( $parent_id ) );
		}
		$trail[] = array( 'label' => get_the_title( $post ), 'url' => get_permalink( $post ) );
	} elseif ( is_category() || is_tag() || is_tax() ) {
		$term    = get_queried_object();
		$trail[] = array( 'label' => $term->name, 'url' => get_term_link( $term ) );
	} elseif ( is_search() ) {
		/* translators: %s: search query */
		$trail[] = array( 'label' => sprintf( __( 'Search: %s', 'acme-theme' ), get_search_query() ), 'url' => get_search_link() );
	} elseif ( is_404() ) {
		$trail[] = array( 'label' => __( 'Not found', 'acme-theme' ), 'url' => home_url( add_query_arg( null, null ) ) );
	}
	return $trail;
}

function acme_breadcrumbs(): void {
	$trail = acme_breadcrumb_trail();
	if ( count( $trail ) < 2 ) {
		return;
	}
	$last = count( $trail ) - 1;

	echo '<nav class="acme-crumbs" aria-label="' . esc_attr__( 'Breadcrumb', 'acme-theme' ) . '"><ol>';
	foreach ( $trail as $i => $crumb ) {
		echo '<li>';
		if ( $i === $last ) {
			echo '<span aria-current="page">' . esc_html( $crumb['label'] ) . '</span>';
		} else {
			echo '<a href="' . esc_url( $crumb['url'] ) . '">' . esc_html( $crumb['label'] ) . '</a>';
		}
		echo '</li>';
	}
	echo '</ol></nav>';

	$items = array();
	foreach ( $trail as $i => $crumb ) {
		$items[] = array( '@type' => 'ListItem', 'position' => $i + 1, 'name' => wp_strip_all_tags( $crumb['label'] ), 'item' => $crumb['url'] );
	}
	printf(
		'<script type="application/ld+json">%s</script>',
		wp_json_encode( array( '@context' => 'https://schema.org', '@type' => 'BreadcrumbList', 'itemListElement' => $items ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
	);
}

How it works

What it solves

Breadcrumbs help visitors move up a level and can appear in search results when marked up properly. This helper produces the trail from the current query, an accessible list and the matching structured data from one source of truth.

How it works

The trail function checks what the current request is: a singular item (with its category or page ancestors), a term archive, a search or a 404. Each branch appends label and URL pairs. The renderer outputs a labelled nav with an ordered list, a link for each crumb except the last, which gets aria-current="page". The same array becomes a BreadcrumbList with positions, encoded with tag-safe JSON flags.

There is exactly one place to change the logic.

Adapting it

Add custom post types by inserting their archive link before the singular title. Use the primary category chosen by your SEO plugin instead of the first one. Style the separators with li + li::before. Hide the trail on the front page, as the count check already does.

Pitfalls

  • Show only one breadcrumb schema per page; remove other plugins' output if both print.
  • The get_post_ancestors order is child first, so it is reversed.
  • Escape the label for HTML and strip tags for JSON.
  • Translated labels must be the visible text.

How to use it

Save as inc/breadcrumbs.php and require it from functions.php.

Call acme_breadcrumbs(); in header.php or a template part below the header.

Style .acme-crumbs ol as an inline list.

Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.

Related snippets