How to Add Breadcrumbs in Drupal with Custom Trails
Breadcrumbs give visitors a clear route through a Drupal website, especially when content is organised into several levels. A typical trail might run from Home to Services, then Bathroom Renovations, and finally the current article. Drupal can generate basic breadcrumbs automatically, but custom trails are often needed when the site structure depends on taxonomy, views, commerce categories, or editorial rules.
A well-built breadcrumb system also helps search engines interpret page relationships and gives users a dependable navigation fallback. This matters for large Australian websites serving different regions, such as a Sydney trades directory, a Melbourne publisher, or a Brisbane community organisation with multiple content sections. The most reliable approach is to use Drupal’s breadcrumb builder system while keeping route handling, cache metadata, and accessibility in mind.
How Drupal builds breadcrumb trails
Drupal renders breadcrumbs through the Breadcrumb block, which is generally available from the block layout screen. The block obtains its links from services tagged with breadcrumb_builder. Core builders can use the current route, menu hierarchy, or entity context to produce a trail.
The default result may be adequate for a small brochure site, but it can become confusing when URLs do not match the editorial hierarchy. For example, /node/245 may belong under a “Guides” section even though that relationship is not visible in the path. A custom builder can inspect the node, its taxonomy terms, or route parameters and then create a meaningful sequence of links.
The breadcrumb block must be placed in a visible region before visitors can see it. In a theme designed for Australian users on smaller mobile connections, keep the markup lightweight and avoid adding a second navigation system that duplicates the main menu. Breadcrumbs should support the page structure rather than compete with it.
Choose a trail strategy
The simplest approach is a menu-based trail. Editors place content inside a menu hierarchy, and Drupal follows that structure when producing links. This works well for documentation portals, government-style information sites, and intranets where one page has one obvious parent.
A taxonomy-based trail is more flexible for news, resources, and product catalogues. A node can belong to a vocabulary such as “Topics”, “Services”, or “Locations”, while the term hierarchy supplies the breadcrumb parents. This is useful for a national organisation with content divided between New South Wales, Victoria, and Queensland, although a site should avoid showing several competing parent terms for the same page.
A route-aware builder is the best option when the trail depends on custom logic. It can distinguish between node pages, views, search results, user profiles, and commerce routes. It can also add a fixed landing page, omit an internal taxonomy term, or use a different label for a route without changing the visible URL.
Before writing code, decide which source is authoritative. Mixing menu parents, taxonomy terms, and URL segments without a clear rule often creates duplicated or circular trails. A practical policy is to use menu hierarchy for structured documentation, taxonomy for reusable content collections, and explicit route logic for special pages.
Create a custom breadcrumb service
A custom module is a clean place for breadcrumb logic. Create a service definition in custom_trails.services.yml and assign it the breadcrumb_builder tag. A high priority allows the service to take precedence when more than one builder applies to the same route.
services:
custom_trails.breadcrumb:
class: Drupal\custom_trails\Breadcrumb\CustomBreadcrumbBuilder
arguments:
- '@entity_type.manager'
tags:
- { name: breadcrumb_builder, priority: 100 }
The builder class should implement BreadcrumbBuilderInterface. The applies() method limits the builder to routes that it understands, while build() creates the links. Restricting the route scope is important because a builder that claims every route can interfere with administration pages, Views, and other contributed modules.
<?php
namespace Drupal\custom_trails\Breadcrumb;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\node\NodeInterface;
final class CustomBreadcrumbBuilder implements BreadcrumbBuilderInterface {
public function __construct(
private EntityTypeManagerInterface $entityTypeManager,
) {}
public function applies(RouteMatchInterface $route_match): bool {
return $route_match->getRouteName() === 'entity.node.canonical'
&& $route_match->getParameter('node') instanceof NodeInterface;
}
public function build(RouteMatchInterface $route_match): Breadcrumb {
$node = $route_match->getParameter('node');
$links = [
Link::createFromRoute('Home', '<front>'),
Link::createFromRoute('Resources', 'view.resources.page_1'),
];
if ($node->hasField('field_topic')
&& !$node->get('field_topic')->isEmpty()) {
$term = $node->get('field_topic')->entity;
if ($term) {
$links[] = Link::fromTextAndUrl(
$term->label(),
$term->toUrl()
);
}
}
$links[] = Link::fromTextAndUrl($node->label(), $node->toUrl());
$breadcrumb = new Breadcrumb();
$breadcrumb->setLinks($links);
$breadcrumb->addCacheContexts(['route', 'url.path']);
$breadcrumb->addCacheableDependency($node);
return $breadcrumb;
}
}
The field name, view route, and vocabulary logic need to match the project. If the content type uses a parent reference rather than field_topic, load the referenced entity and walk up its parent chain. Each term should be converted into a Link so that the breadcrumb remains a proper navigation element rather than a string assembled in a Twig template.
| Approach | Best fit | Main benefit | Common limitation |
|---|---|---|---|
| Menu hierarchy | Documentation and fixed page trees | Easy for editors to understand | One page usually has one parent |
| Taxonomy hierarchy | Articles, resources, and catalogues | Reusable and scalable classification | Multiple terms can create ambiguity |
| URL segments | Sites with stable path patterns | Quick to derive from aliases | URLs may not reflect editorial relationships |
| Custom service | Mixed or conditional structures | Full control over routes and labels | Requires testing and cache awareness |
Shape breadcrumbs with preprocess logic
A service is preferable when the trail itself must be calculated, but theme preprocess logic can adjust presentation. In THEMENAME_preprocess_breadcrumb(), you can add a CSS class, alter a label, or remove the current-page link from the rendered variables. Avoid using preprocess code as the primary place for entity loading and hierarchy decisions, because that makes the behaviour harder to reuse and test.
Twig should receive already-prepared breadcrumb data. A small template override can control separators, responsive visibility, and microdata attributes while preserving semantic navigation:
{% if breadcrumb %}
<nav class="breadcrumb" aria-label="{{ 'Breadcrumb'|t }}">
<ol>
{% for item in breadcrumb %}
<li>
{% if item.url %}
<a href="{{ item.url }}">{{ item.text }}</a>
{% else %}
<span aria-current="page">{{ item.text }}</span>
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
{% endif %}
Use aria-current="page" for the current location and ensure the separator is decorative rather than part of the link text. On narrow screens, a CSS rule can hide intermediate items or allow horizontal scrolling, but the full logical trail should remain available to assistive technology where practical.
Custom labels may also come from configuration. For example, a route called “Locations” might display “Service areas” in the breadcrumb, while a page about bathroom design could sit beneath a “Home improvement” term. A content team could link to a relevant spa-like bathroom atmosphere resource from that branch without forcing the external page into the Drupal trail.
Keep caching and access rules correct
Breadcrumbs are renderable output, so cache metadata matters. If a trail changes according to the current route, add the route cache context. If it depends on the path, query parameters, language, user permissions, or an account-specific condition, add the relevant contexts as well. A missing context can cause one visitor’s trail to appear for another visitor.
Referenced nodes and taxonomy terms should be added as cacheable dependencies. Drupal can then invalidate the breadcrumb when an entity label, alias, or relationship changes. If the builder uses configuration, inject the configuration factory and add the configuration object as a dependency where appropriate.
Do not build a breadcrumb with raw URLs from user input. Use entity toUrl() methods or Link::createFromRoute() so that route parameters, language negotiation, and access checks follow Drupal’s normal mechanisms. If an entity is unpublished or inaccessible, decide whether it should be omitted from the trail or replaced by a permitted parent.
Caching becomes especially important for high-traffic sites in Australia, where a campaign can send large bursts of visitors from Perth, Adelaide, or regional areas at once. Clear the relevant caches after changing service definitions, and inspect the rendered output rather than assuming that a successful PHP rebuild means the visible breadcrumb is correct.
Validate trails across real site content
Test every route type covered by the builder. Check a published node, an unpublished node viewed by an administrator, a node without a taxonomy term, a term with no parent, and a page whose title contains accented characters or punctuation. Also test translated content if the site uses English and another language, because labels and aliases can vary by language.
A local-market site may have regional landing pages such as “Sydney”, “Melbourne”, or “Gold Coast”. Confirm that the regional term links to the correct canonical page and that a node assigned to multiple locations follows the documented primary-location rule. For ecommerce websites, test category pages, product pages, filtered listings, and unavailable products separately.
A practical validation checklist
- Confirm the Breadcrumb block is placed in the intended theme region.
- Verify the current page is identified with
aria-current="page". - Check that every parent link returns a successful response.
- Rebuild caches after changing the service or routing configuration.
The following checks help catch editorial and performance problems before release:
- Test content with missing, archived, or duplicated parent terms.
- Inspect cache contexts and tags in the rendered response.
- Compare desktop and mobile output at common Australian breakpoints.
- Review the trail with keyboard navigation and a screen reader.
When the site consumes external data, keep that work separate from breadcrumb presentation. A dedicated service can normalise API responses before content is rendered; the API integration guide provides a useful pattern for structuring that kind of Drupal code. The breadcrumb builder should receive stable entities or values rather than making a remote request during every page render.
Finally, clear Drupal caches, inspect the page source, and confirm that the breadcrumb appears once. Check that the menu, page title, canonical link, and structured data agree about the page’s position. A custom trail is successful when it reflects the editorial model, remains fast under cache, and gives both Australian visitors and search engines a consistent path through the site.