Breadcrumbs can be built without a plugin. Write one function in functions.php and call it from your templates. Since it has no plugin dependency, it’s safe to include in a theme you distribute or deliver.
Here’s the finished code first.
/**
* Output breadcrumbs.
* Each item is ['label' => display name, 'url' => link target (null for the last item)]
*/
function my_breadcrumb() {
if (is_front_page()) {
return; // don't show them on the front page
}
$items = [['label' => 'Home', 'url' => home_url('/')]];
if (is_home()) {
// the post archive
$items[] = ['label' => get_the_title(get_option('page_for_posts')) ?: 'News', 'url' => null];
} elseif (is_category() || is_tag() || is_tax()) {
$items[] = ['label' => single_term_title('', false), 'url' => null];
} elseif (is_post_type_archive()) {
$items[] = ['label' => post_type_archive_title('', false), 'url' => null];
} elseif (is_search()) {
$items[] = ['label' => 'Search results for "' . get_search_query() . '"', 'url' => null];
} elseif (is_404()) {
$items[] = ['label' => 'Page not found', 'url' => null];
} elseif (is_page()) {
// walk up the parent pages
foreach (array_reverse(get_post_ancestors(get_the_ID())) as $ancestor) {
$items[] = ['label' => get_the_title($ancestor), 'url' => get_permalink($ancestor)];
}
$items[] = ['label' => get_the_title(), 'url' => null];
} elseif (is_singular()) {
$type = get_post_type();
if ($type === 'post') {
// posts get one category level in between
$cats = get_the_category();
if ($cats) {
$items[] = ['label' => $cats[0]->name, 'url' => get_category_link($cats[0]->term_id)];
}
} else {
// custom post types get their archive page in between
$obj = get_post_type_object($type);
if ($obj && $obj->has_archive) {
$items[] = ['label' => $obj->labels->name, 'url' => get_post_type_archive_link($type)];
}
}
$items[] = ['label' => get_the_title(), 'url' => null];
}
// output
echo '<nav class="breadcrumb" aria-label="Breadcrumb"><ol>';
foreach ($items as $item) {
echo '<li>';
if ($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>';
}Call it from a template in one line.
<?php my_breadcrumb(); ?>The thinking behind the code
What it does is simple.
- Build an array—start with “Home” and add items in hierarchical order
- Don’t link the last item (it’s the page you’re on)
- Output the whole thing at once
The key is that assembling and rendering are separated. ol and li express the hierarchy, and adding aria-label and aria-current means the position comes across to screen readers too.
Adding the CSS
.breadcrumb ol {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
list-style: none;
padding: 0;
margin: 0 0 1.5rem;
font-size: 0.875rem;
color: #666;
}
.breadcrumb li + li::before {
content: "›";
margin-right: 0.5em;
color: #aaa;
}
.breadcrumb a { color: inherit; }The trick with the separator is to insert it via CSS ::before. Write > into the HTML and it gets read aloud unnecessarily, or mixed in when someone copies the text.
Adding structured data
Making your breadcrumbs understandable to Google means they sometimes appear in search results. Output them as JSON-LD.
function my_breadcrumb_jsonld($items) {
$list = [];
foreach ($items as $i => $item) {
$entry = [
'@type' => 'ListItem',
'position' => $i + 1,
'name' => $item['label'],
];
if ($item['url']) {
$entry['item'] = $item['url'];
}
$list[] = $entry;
}
$data = [
'@context' => 'https://schema.org',
'@type' => 'BreadcrumbList',
'itemListElement' => $list,
];
echo '<script type="application/ld+json">'
. wp_json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
. '</script>';
}Call this inside my_breadcrumb() above, after the HTML has been output.
Getting the post archive’s name right
When “Settings” → “Reading” assigns the posts page to a static page, using that page’s title in the breadcrumbs is the natural choice.
$blog_id = (int) get_option('page_for_posts');
$label = $blog_id ? get_the_title($blog_id) : 'News';On single posts too, slotting that level in before the category makes it more accurate (adjust it to match your site’s structure).
Compared with using a plugin
- Your own code—complete control over the HTML. No extra plugin. Can ship inside the theme
- A plugin—adjustable from a settings screen. No branching to build
SEO plugins often include a breadcrumb feature, so if you already have one, the important thing is not to duplicate it. If you’re building your own, turn the plugin’s feature off.
Summary
- Breadcrumbs are one function in
functions.php—no extra plugin dependency - Stack the hierarchy in an array and leave only the last item unlinked—that’s the implementation pattern
- Mark it up with
olplusaria-current, and insert separators via CSS::before - Adding structured data (BreadcrumbList) can get them into search results. Keep it identical to what’s displayed
A path that shows people where they are translates directly into how much of the site they explore.