Add breadcrumbs to your WordPress theme without a plugin
Breadcrumb navigation gives visitors a compact trail from the home page to the page they are viewing. It helps people understand where a product, article or nested page sits within the website, and it gives search engines another clear set of internal relationships. In a WordPress theme, this hierarchy can be generated from core template data without adding another plugin.
The useful starting point is to mirror how visitors already move through the website. A service business in Sydney might place “Home, Services, Plumbing” above a location page, while a Melbourne publisher might use “Home, News, Industry updates”. These labels should describe the site’s language rather than expose internal post types, database terms or administrative structures.
Adding breadcrumbs to a WordPress theme without a plugin is mostly a matter of collecting trail items, escaping their labels and URLs, and printing them as an ordered list. Australian sites should also use local spelling such as “customise” and “organisation”, while retailers may need category labels that make sense to customers shopping in Australian dollars with GST included.
Plan the navigation hierarchy
Choose where the navigation will appear before writing any PHP. A common position is directly below the site header or beside the page title. This keeps the trail visible without competing with the main menu, particularly on phones where vertical space is limited. The exact placement depends on the theme’s header template and the design of each page template.
WordPress content rarely follows one uniform structure. Pages may have parent and child relationships, posts usually belong to categories, WooCommerce products can sit inside product categories, and custom post types may have their own archives. The WordPress template hierarchy and conditional tags can identify each situation, but the trail should present only the relationships that help a visitor understand the current location.
A practical hierarchy might include:
- Home, parent page and child page for nested pages
- Home, category and post for editorial content
- Home, product category and product for store content
- Home and the current archive for category, tag or date listings
Use a child theme where possible so the implementation survives parent theme updates. If the parent theme already provides a related action hook, such as wp_body_open, it can be used without modifying the template structure.
Build the trail in functions.php
Add the following functions to the child theme’s functions.php file. Replace the mytheme text domain with the theme’s actual text domain if it uses a different one. The prefix also prevents collisions with functions supplied by another theme.
function mytheme_add_breadcrumb( &$items, $label, $url = '' ) {
$items[] = array(
'label' => (string) $label,
'url' => $url,
);
}
function mytheme_breadcrumb_items() {
$items = array();
if ( is_front_page() ) {
return $items;
}
mytheme_add_breadcrumb(
$items,
__( 'Home', 'mytheme' ),
home_url( '/' )
);
if ( is_home() ) {
$blog_id = (int) get_option( 'page_for_posts' );
if ( $blog_id ) {
mytheme_add_breadcrumb(
$items,
get_the_title( $blog_id ),
get_permalink( $blog_id )
);
} else {
mytheme_add_breadcrumb( $items, __( 'Blog', 'mytheme' ) );
}
} elseif ( is_singular() ) {
$post_id = get_queried_object_id();
$ancestors = array_reverse( get_post_ancestors( $post_id ) );
foreach ( $ancestors as $ancestor_id ) {
if ( (int) get_option( 'page_on_front' ) === $ancestor_id ) {
continue;
}
mytheme_add_breadcrumb(
$items,
get_the_title( $ancestor_id ),
get_permalink( $ancestor_id )
);
}
$taxonomy = '';
if ( is_singular( 'post' ) ) {
$taxonomy = 'category';
} elseif ( is_singular( 'product' ) && taxonomy_exists( 'product_cat' ) ) {
$taxonomy = 'product_cat';
}
if ( $taxonomy ) {
$terms = get_the_terms( $post_id, $taxonomy );
if ( $terms && ! is_wp_error( $terms ) ) {
$term = reset( $terms );
$term_ancestors = array_reverse(
get_ancestors( $term->term_id, $taxonomy, 'taxonomy' )
);
foreach ( $term_ancestors as $ancestor_id ) {
$ancestor = get_term( $ancestor_id, $taxonomy );
if ( $ancestor && ! is_wp_error( $ancestor ) ) {
mytheme_add_breadcrumb(
$items,
$ancestor->name,
get_term_link( $ancestor )
);
}
}
mytheme_add_breadcrumb( $items, $term->name, get_term_link( $term ) );
}
}
mytheme_add_breadcrumb( $items, get_the_title( $post_id ) );
} elseif ( is_category() || is_tax() ) {
$term = get_queried_object();
$taxonomy = $term->taxonomy;
$ancestors = array_reverse(
get_ancestors( $term->term_id, $taxonomy, 'taxonomy' )
);
foreach ( $ancestors as $ancestor_id ) {
$ancestor = get_term( $ancestor_id, $taxonomy );
if ( $ancestor && ! is_wp_error( $ancestor ) ) {
mytheme_add_breadcrumb(
$items,
$ancestor->name,
get_term_link( $ancestor )
);
}
}
mytheme_add_breadcrumb( $items, $term->name );
} elseif ( is_tag() ) {
$term = get_queried_object();
mytheme_add_breadcrumb( $items, $term->name );
} elseif ( is_post_type_archive() ) {
$post_type = get_query_var( 'post_type' );
if ( is_array( $post_type ) ) {
$post_type = reset( $post_type );
}
$object = $post_type ? get_post_type_object( $post_type ) : null;
if ( $object ) {
mytheme_add_breadcrumb(
$items,
$object->labels->name,
get_post_type_archive_link( $post_type )
);
}
} elseif ( is_author() ) {
$author = get_queried_object();
mytheme_add_breadcrumb(
$items,
$author->display_name,
get_author_posts_url( $author->ID )
);
} elseif ( is_date() ) {
$year = (int) get_query_var( 'year' );
$month = (int) get_query_var( 'monthnum' );
$day = (int) get_query_var( 'day' );
$date = new DateTimeImmutable( 'now', wp_timezone() );
$date = $date->setDate(
$year,
$month ? $month : 1,
$day ? $day : 1
);
if ( $day ) {
$label = $date->format( 'j F Y' );
} elseif ( $month ) {
$label = $date->format( 'F Y' );
} else {
$label = $year;
}
mytheme_add_breadcrumb( $items, $label );
} elseif ( is_search() ) {
mytheme_add_breadcrumb(
$items,
sprintf(
__( 'Search results for “%s”', 'mytheme' ),
get_search_query()
)
);
} elseif ( is_404() ) {
mytheme_add_breadcrumb( $items, __( 'Page not found', 'mytheme' ) );
}
return $items;
}
The builder uses get_post_ancestors() for nested content, get_the_terms() for categories and product categories, and the template hierarchy for archive pages. The filter applied during rendering also provides a clean extension point for custom post types, additional taxonomies or a site-specific “Services” parent that does not exist as a WordPress page.
Render accessible breadcrumb markup
The trail should use an ordered list inside a labelled navigation region. Each linked item represents an ancestor, while the final item represents the current page and should not link back to itself. Escaping both labels and URLs prevents malformed or unexpected content from entering the page.
function mytheme_breadcrumbs() {
$items = apply_filters(
'mytheme_breadcrumb_items',
mytheme_breadcrumb_items()
);
if ( ! is_array( $items ) || count( $items ) < 2 ) {
return;
}
$last_index = count( $items ) - 1;
echo '<nav class="breadcrumbs" aria-label="'
. esc_attr__( 'Breadcrumb', 'mytheme' ) . '"><ol>';
foreach ( $items as $index => $item ) {
echo '<li>';
if ( $index < $last_index && ! empty( $item['url'] ) ) {
echo '<a href="' . esc_url( $item['url'] ) . '">'
. esc_html( $item['label'] ) . '</a>';
} else {
echo '<span aria-current="page">'
. esc_html( $item['label'] ) . '</span>';
}
echo '</li>';
}
echo '</ol></nav>';
}
For a modern block theme, the renderer can be attached to the wp_body_open action:
add_action( 'wp_body_open', 'mytheme_breadcrumbs' );
A classic theme can call mytheme_breadcrumbs() directly in header.php, immediately after the site masthead. Use one placement method rather than both, otherwise the navigation will be printed twice. The aria-current="page" attribute identifies the final item for assistive technology, while the ordered-list structure communicates the hierarchy without relying on visual separators alone.
Style the trail for every screen
Breadcrumb text is usually smaller than regular navigation, but it must remain readable. A restrained colour, comfortable line height and visible keyboard focus state will keep it usable without competing with the main menu. CSS custom properties allow the trail to inherit theme colours rather than introducing a separate visual system.
.breadcrumbs {
padding-block: 0.75rem;
font-size: 0.9375rem;
}
.breadcrumbs ol {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem 0.7rem;
margin: 0;
padding: 0;
list-style: none;
}
.breadcrumbs li {
display: flex;
align-items: center;
}
.breadcrumbs li + li::before {
content: "/";
margin-right: 0.7rem;
color: #6b7280;
}
.breadcrumbs a {
color: #1d4ed8;
text-decoration-thickness: 0.08em;
text-underline-offset: 0.16em;
}
.breadcrumbs a:focus-visible {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.breadcrumbs [aria-current="page"] {
color: #374151;
}
@media (max-width: 37.5rem) {
.breadcrumbs {
font-size: 0.875rem;
}
}
The flex layout allows a long trail to wrap onto another line on a narrow Australian mobile device without hiding any information. If the theme uses a different separator, it can be supplied through a pseudo-element, but the text should remain understandable when CSS fails to load. Avoid fixed widths and horizontal scrolling because category names such as “Building and Construction Supplies” can become lengthy.
Test common WordPress journeys
Check the navigation with real content rather than a newly created sample page. Create a parent and child page, assign a post to a nested category, and view a WooCommerce product through its product category. For a Brisbane store, confirm that the shop archive uses a familiar label such as “Shop” and that product categories reflect how Australian customers browse the catalogue.
Date archives also need attention. A Sydney publication using Australia/Sydney and an Adelaide publisher using Australia/Adelaide should see labels generated in the site’s configured timezone, not the web server’s default timezone. Search queries, author archives and 404 pages should each have a sensible final item. Developers maintaining the theme from a Mac can keep Mac productivity resources alongside their local PHP and browser workflow.
The final testing pass should cover these routes:
- The front page, which normally needs no visible trail
- Nested pages, posts, categories and custom post types
- Search results, author pages, date archives and missing pages
- WooCommerce shop archives, product categories and individual products
The function, template hook and stylesheet then operate as one native theme feature. WordPress content changes are reflected automatically, and the resulting trail remains light, accessible and maintainable without a dedicated breadcrumbs plugin.