Tips・The foundations of WordPress customization

Building breadcrumbs without a plugin

Code for building your own breadcrumbs in functions.php, covering posts, static pages, categories and custom post types. Includes the CSS and how to add structured data that works in search results.

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.

  1. Build an array—start with “Home” and add items in hierarchical order
  2. Don’t link the last item (it’s the page you’re on)
  3. 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.

TipsWordPress conditional tags cheat sheetThe cheat sheet for the is_… conditional tags used in the branching

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

  1. Breadcrumbs are one function in functions.php—no extra plugin dependency
  2. Stack the hierarchy in an array and leave only the last item unlinked—that’s the implementation pattern
  3. Mark it up with ol plus aria-current, and insert separators via CSS ::before
  4. 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.

FAQ

Do breadcrumbs help with SEO?
They communicate your site structure to search engines, and with structured data added, breadcrumbs sometimes appear in search results. The bigger practical benefit is that visitors can see where they are—and they work as internal links too.
Can I build them without a plugin?
Yes. Write one function in functions.php and call it from your templates. You use conditional tags to determine the page type and assemble the links for each type.
Can it handle custom post types?
Yes. Determine the post type with is_singular(), then pull the archive page's label and URL from get_post_type_object() and slot them into the middle level.
How do I add structured data?
Output each breadcrumb item as a BreadcrumbList in JSON-LD. Google's policy is that it must match the breadcrumbs you actually display.