If you are tired of installing bulky SEO plugins just to display a simple navigation trail, you are in the right place. In this tutorial, we will show you exactly how to add WordPress breadcrumbs without a plugin, using only PHP inside your theme’s functions.php file, plus proper JSON-LD schema markup so Google can display them in search results.
This method is lightweight, fast, and gives you full control over the HTML output. No plugin bloat, no unnecessary database queries, just clean code that works.
Why Code Breadcrumbs Instead of Using a Plugin?
Plugins like Yoast or RankMath do offer breadcrumb features, but they come with a full ecosystem you might not need. Here is a quick comparison:
| Aspect | Plugin Method | Custom PHP Method |
|---|---|---|
| Performance | Extra queries and assets loaded | Minimal footprint |
| Customization | Limited to plugin options | Full control over HTML and CSS |
| Schema Markup | Often included but generic | Tailored JSON-LD output |
| Dependencies | Tied to plugin updates | Independent, portable code |

Step 1: Add the Breadcrumb Function to functions.php
Open your child theme’s functions.php file (never edit the parent theme directly) and paste the following function. It handles posts, pages, categories, tags, custom post types, author archives, search results, and 404 pages.
function express_custom_breadcrumbs() {
$separator = '»';
$home_title = 'Home';
$home_url = home_url();
global $post;
$output = '<nav class="express-breadcrumbs" aria-label="Breadcrumb">';
$output .= '<a href="' . $home_url . '">' . $home_title . '</a> ' . $separator . ' ';
if ( is_category() ) {
$output .= single_cat_title('', false);
} elseif ( is_tag() ) {
$output .= single_tag_title('', false);
} elseif ( is_author() ) {
$output .= 'Author: ' . get_the_author();
} elseif ( is_search() ) {
$output .= 'Search results for: ' . get_search_query();
} elseif ( is_404() ) {
$output .= 'Page not found';
} elseif ( is_single() ) {
$post_type = get_post_type();
if ( $post_type != 'post' ) {
$pt_obj = get_post_type_object($post_type);
$output .= '<a href="' . get_post_type_archive_link($post_type) . '">' . $pt_obj->labels->singular_name . '</a> ' . $separator . ' ';
} else {
$categories = get_the_category();
if ( $categories ) {
$cat = $categories[0];
$output .= '<a href="' . get_category_link($cat->term_id) . '">' . $cat->name . '</a> ' . $separator . ' ';
}
}
$output .= get_the_title();
} elseif ( is_page() ) {
if ( $post->post_parent ) {
$ancestors = array_reverse(get_post_ancestors($post->ID));
foreach ( $ancestors as $ancestor ) {
$output .= '<a href="' . get_permalink($ancestor) . '">' . get_the_title($ancestor) . '</a> ' . $separator . ' ';
}
}
$output .= get_the_title();
}
$output .= '</nav>';
return $output;
}
Step 2: Display the Breadcrumbs in Your Theme
Now you need to call this function where you want the breadcrumbs to appear. Typical locations include:
- header.php – right below the main navigation
- single.php – above the post title
- page.php – above the page content
- archive.php – above the archive listing
Add this line where you want the trail to appear:
<?php echo express_custom_breadcrumbs(); ?>
Using a Shortcode Instead
If you prefer inserting breadcrumbs via the block editor or a widget, register a shortcode:
function express_breadcrumbs_shortcode() {
return express_custom_breadcrumbs();
}
add_shortcode('breadcrumbs', 'express_breadcrumbs_shortcode');
Then simply use [breadcrumbs] anywhere in your content.

Step 3: Add Schema Markup for SEO
This is where most tutorials stop, but Google needs structured data to actually display breadcrumbs in the search snippet. We will use JSON-LD BreadcrumbList schema, the format Google officially recommends.
Add this function to your functions.php:
function express_breadcrumbs_schema() {
if ( is_front_page() ) return;
$items = array();
$position = 1;
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => 'Home',
'item' => home_url()
);
if ( is_single() ) {
$categories = get_the_category();
if ( $categories ) {
$cat = $categories[0];
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => $cat->name,
'item' => get_category_link($cat->term_id)
);
}
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => get_the_title(),
'item' => get_permalink()
);
} elseif ( is_page() ) {
global $post;
if ( $post->post_parent ) {
$ancestors = array_reverse(get_post_ancestors($post->ID));
foreach ( $ancestors as $ancestor ) {
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => get_the_title($ancestor),
'item' => get_permalink($ancestor)
);
}
}
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => get_the_title(),
'item' => get_permalink()
);
} elseif ( is_category() ) {
$items[] = array(
'@type' => 'ListItem',
'position' => $position++,
'name' => single_cat_title('', false),
'item' => get_category_link(get_queried_object_id())
);
}
$schema = array(
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $items
);
echo '<script type="application/ld+json">' . wp_json_encode($schema) . '</script>';
}
add_action('wp_head', 'express_breadcrumbs_schema');
Step 4: Style Your Breadcrumbs
Add this to your theme’s style.css file to make the breadcrumbs look clean and readable:
.express-breadcrumbs {
font-size: 14px;
color: #666;
padding: 12px 0;
margin-bottom: 20px;
}
.express-breadcrumbs a {
color: #0073aa;
text-decoration: none;
}
.express-breadcrumbs a:hover {
text-decoration: underline;
}

Step 5: Test Your Breadcrumbs
Before considering the job done, run through these checks:
- Visit a post, a page, a category archive, and a 404 page. Confirm the trail matches the actual hierarchy.
- Open the page source and verify the JSON-LD script is present in the
<head>. - Use the Google Rich Results Test at search.google.com/test/rich-results to validate the BreadcrumbList schema.
- Check the Schema Markup Validator at validator.schema.org for any warnings.
- Submit the updated pages via Google Search Console to speed up re-indexing.
Handling Custom Post Types and Taxonomies
The function above already handles custom post types by pulling the archive link. If your CPT uses a custom taxonomy (like a WooCommerce product with product categories), extend the is_single() block like this:
if ( is_singular('product') ) {
$terms = get_the_terms(get_the_ID(), 'product_cat');
if ( $terms && !is_wp_error($terms) ) {
$term = array_shift($terms);
$output .= '<a href="' . get_term_link($term) . '">' . $term->name . '</a> ' . $separator . ' ';
}
}

Common Mistakes to Avoid
- Editing the parent theme directly. Your changes will be lost on the next update. Always use a child theme.
- Duplicating schema. If another plugin already outputs BreadcrumbList schema, remove one of them to avoid conflicts.
- Forgetting the aria-label. Accessibility matters, and screen readers rely on it.
- Hardcoding URLs. Always use
home_url()andget_permalink()so the code works across environments.
FAQ
Do breadcrumbs actually help SEO?
Yes. Google uses BreadcrumbList schema to replace the URL in search snippets, which improves click-through rates. Breadcrumbs also help search engines understand your site’s structure and internal linking.
Will this code slow down my WordPress site?
No. The function only runs when called and uses standard WordPress core functions. There are no extra database queries or asset files loaded, unlike most breadcrumb plugins.
Can I use this method with a Full Site Editing (FSE) theme?
Yes, but you will need to register the shortcode as shown in Step 2 and insert it via a Shortcode block, since FSE themes do not use traditional PHP templates. Alternatively, create a custom block that outputs the breadcrumb function. There’s a good explainer over at generate.support.
How do I remove the breadcrumbs from the homepage?
Wrap the display call in a conditional: <?php if ( !is_front_page() ) echo express_custom_breadcrumbs(); ?>. The schema function already excludes the homepage automatically.
What if my breadcrumbs are not showing in Google search results?
Google may take a few weeks to process the schema. Make sure the JSON-LD validates without errors, request re-indexing in Search Console, and be patient. Google also decides when to display breadcrumbs based on relevance. striviothemes.com has a solid rundown on this.
Can I add icons or arrows between items?
Absolutely. Replace the $separator variable with any HTML you want, such as <span class="sep">›</span> or an SVG icon.
Wrapping Up
Adding WordPress breadcrumbs without a plugin is not just possible, it is often the better choice for developers who care about performance and clean code. With about 50 lines of PHP, you now have a fully functional, SEO-friendly breadcrumb system with proper schema markup. No plugin dependencies, no bloat, just results. There’s a good explainer over at seopress.org.
Copy the code, adapt the styling to match your theme, validate the schema, and you are done. Your site will be faster and Google will thank you for it.
