Best Practices & Anti-Patterns
When to Use
Read this before writing any breadcrumb-related code. These are the patterns that cause production issues and the ones that solve them.
Decision
| Concern | Anti-pattern | Correct pattern |
|---|---|---|
| XSS in titles | new Link(Markup::create('<b>' . $node->getTitle() . '</b>'), $url) |
new Link($node->getTitle(), $url) — Twig escapes it |
| Access bypass | Custom builder creates links without checking access | Replicate $this->accessManager->check() per segment |
Entity loading in applies() |
\Drupal::entityTypeManager()->getStorage('node')->load($nid) |
$route_match->getParameter('node') — param converter already loaded it |
| Static calls in a builder | \Drupal::service(...) in constructor logic |
Inject services via arguments: in the service definition |
| Empty breadcrumb with no contexts | return new Breadcrumb(); |
Always add addCacheContexts(['route']) before returning |
| Priority collision | Priority 0 custom builder alongside PathBasedBreadcrumbBuilder |
Use priority 1+ to guarantee ordering |
Pattern
Access check in custom builders:
$access = $this->accessManager->check($route_match, $this->currentUser, NULL, TRUE);
$breadcrumb->addCacheableDependency($access);
if ($access->isAllowed()) {
$links[] = new Link($title, $url);
}
Proper service injection:
# my_module.services.yml
services:
my_module.product_breadcrumb:
class: Drupal\my_module\Breadcrumb\ProductBreadcrumbBuilder
arguments: ['@entity_type.manager', '@current_user', '@access_manager']
tags:
- { name: breadcrumb_builder, priority: 200 }
Safe empty breadcrumb return:
$breadcrumb = new Breadcrumb();
$breadcrumb->addCacheContexts(['route']);
return $breadcrumb;
Common Mistakes
- Wrong: Priority 0 custom builder that should run before
PathBasedBreadcrumbBuilder→ Right: Both are priority 0; use 1+ to guarantee ordering - Wrong:
applies(RouteMatchInterface $route_match): boolwithout the nullableCacheableMetadataparameter → Right: Declare?CacheableMetadata $cacheable_metadata = NULL; crashes on Drupal 10.4+ otherwise - Wrong: Assuming
hook_system_breadcrumb_alter()changes are reflected in Easy Breadcrumb's JSON-LD → Right:EasyBreadcrumbStructuredDataJsonLdcallsmoduleHandler->alter()itself, so the alter does run — but it is a separate call; verify behavior if you have both - Wrong: Calling
$block->build()inhook_page_attachments→ Right: Renders the block outside the render pipeline and discards cache metadata
When Not to Use Breadcrumbs
- Single-level sites (Home → page only) — the breadcrumb just shows "Home"; disable via
hide_single_home_item - Search results or Views with filters — breadcrumbs would show the filter path, which is meaningless to users
- Modal or AJAX-loaded content — breadcrumbs should reflect page hierarchy, not partial content state