Customizing the breadcrumb trail with Drupal 8

A road

By default, Drupal 8 has two methods for building the breadcrumb trail. For content, this method is based on the URL of the page, and for taxonomy terms this method is based on the vocabulary hierarchy.

The default construction of a breadcrumb

Let's explore in more detail the construction of the breadcrumb for content.

Let's take an example of a content page with the following URL:

/services/freelance/drupal/webfactory-drupal

The last part of the URL (webfactory-drupal) corresponds to the title of the page. Drupal will then inspect the rest of the URL and for each part look for if a content matches that URL.

So, Drupal will inspect this URL, to see if it matches existing content.

/services/freelance/drupal

If so (let's imagine that a content whose title is Drupal Specialist has this URL), the title of the page is added to the breadcrumb trail.

Then, he inspects this URL, to see if it matches existing content.

/services/freelance

If so (the content title is Freelance Drupal for example), the page title is added to the breadcrumb trail.

And finally Drupal inspects the last part of the URL, to see if it still matches existing content.

/services

The page title (Services) is then added to the breadcrumb trail.

So for this example, if each part of the path corresponds to an existing content page, the breadcrumb trail generated for this URL will be the following.

Home > Services > Freelance Drupal > Drupal specialist > Web factory Drupal

It is thus possible to build a custom-made, relevant breadcrumb trail using this detection by parent path, either by using a manual alias for listing pages, pivot pages or landing pages, or by using the Pathauto module to automatically build a relevant alias for content to be automatically placed in a section of a site (typical example, news, events, services, etc.). 

Note that the generation of the last part of the breadcrumb trail, namely the title of the current page, Web factory Drupal in our example, is the responsibility of the theme. As a general rule, you will find an option in any correct theme that allows you to display or not the title of the current page in the breadcrumb trail. Or it can be done with a simple hook.

/**
 * Implements hook_preprocess_HOOK().
 */
function MY_THEME_preprocess_breadcrumb(&$variables) {
  $request = \Drupal::request();
  $route_match = \Drupal::routeMatch();
  $page_title = \Drupal::service('title_resolver')->getTitle($request, $route_match->getRouteObject());

  $variables['#cache']['contexts'][] = 'url';
  if (count($variables['breadcrumb']) <= 1) {
    $variables['breadcrumb'] = [];
  }
  else {
    $breadcrumb_title = theme_get_setting('breadcrumb_title');
    if ($breadcrumb_title) {
      $variables['breadcrumb'][] = array(
        'text' => $page_title
      );
    }
  }
}

The breadcrumb trail for the pages of taxonomy terms is built according to a different logic: according to the hierarchy of terms and this regardless of the alias used for the pages of terms.

These two methods of breadcrumb generation are the default methods included in Drupal Core. It is of course possible to modify this default behavior by means of contributed modules, such as Breadcrumb Menu for example which generates the breadcrumb depending on the position of the page in the main menu and which in the absence of the page in the menu switches to the default Drupal Core generation, or by means of a custom module.

Customize the breadcrumb trail with a module

Altering the construction of the breadcrumb is done by means of a service tagged with the breadcrumb_builder tag. For example, for example

my_module.term_breadcrumb:
  class: Drupal\my_module\MyModuleTermBreadcrumbBuilder
  arguments: ['@entity_type.manager', '@entity.repository', '@config.factory', '@path.validator', '@path.alias_manager']
  tags:
  - { name: breadcrumb_builder, priority: 1010 }

The priority given to a service of this type makes it possible to order which rules to apply first, the highest priorities being those applied first.

The MyModuleTermBreadcrumbBuilder Class must implement two methods

  • The applies() method that will allow us to indicate when to apply this rule of construction of the breadcrumb
  • The build() method that will build the breadcrumb itself.

So if we want to add a parent to the breadcrumb trail of taxonomy terms pages, for example, our class will look like this.

/**
 * Provides a custom taxonomy breadcrumb builder that uses the term hierarchy.
 */
class MyModuleTermBreadcrumbBuilder implements BreadcrumbBuilderInterface {
  use StringTranslationTrait;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */
  protected $entityTypeManager;

  /**
   * The entity repository.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected $entityRepository;

  /**
   * Drupal\Core\Config\ConfigFactoryInterface definition.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected  $configFactory;

  /**
   * The taxonomy storage.
   *
   * @var \Drupal\Taxonomy\TermStorageInterface
   */
  protected $termStorage;

  /**
   * The settings of my module taxonomy configuration.
   *
   * @var \Drupal\Core\Config\Config
   */
  protected $taxonomySettings;

  /**
   * The path validator service.
   *
   * @var \\Drupal\Core\Path\PathValidatorInterface
   */
  protected $pathValidator;

  /**
   * The alias manager.
   *
   * @var \Drupal\Core\Path\AliasManagerInterface
   */
  protected $aliasManager;

  /**
   * MyModuleTermBreadcrumbBuilder constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   * @param \Drupal\Core\Path\PathValidatorInterface $path_validator
   * @param \Drupal\Core\Path\AliasManagerInterface $alias_manager
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, ConfigFactoryInterface $config_factory, PathValidatorInterface $path_validator, AliasManagerInterface $alias_manager) {
    $this->entityTypeManager = $entity_type_manager;
    $this->entityRepository = $entity_repository;
    $this->configFactory = $config_factory;
    $this->pathValidator = $path_validator;
    $this->aliasManager = $alias_manager;
    $this->termStorage = $this->entityTypeManager->getStorage('taxonomy_term');
    $this->taxonomySettings = $this->configFactory->get('my_module.taxonomy_settings');
  }

  /**
   * {@inheritdoc}
   */
  public function applies(RouteMatchInterface $route_match) {
    return $route_match->getRouteName() == 'entity.taxonomy_term.canonical'
      && $route_match->getParameter('taxonomy_term') instanceof TermInterface;
  }

  /**
   * {@inheritdoc}
   */
  public function build(RouteMatchInterface $route_match) {
    $breadcrumb = new Breadcrumb();
    $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
    /** @var \Drupal\taxonomy\TermInterface $term */
    $term = $route_match->getParameter('taxonomy_term');
    $breadcrumb_parent = $this->taxonomySettings->get('vocabularies.' . $term->bundle() . '.breadcrumb_parent');
    if ($breadcrumb_parent) {
      $url = $this->pathValidator->getUrlIfValid($breadcrumb_parent);
      if ($this->pathValidator->isValid($breadcrumb_parent)) {
        $path = $this->aliasManager->getPathByAlias($breadcrumb_parent);
        if(preg_match('/node\/(\d+)/', $path, $matches)) {
          $node = Node::load($matches[1]);
          if ($node instanceof NodeInterface) {
            $node = $this->entityRepository->getTranslationFromContext($node);
            $breadcrumb->addCacheableDependency($node);
            $breadcrumb->addLink(Link::createFromRoute($node->label(), 'entity.node.canonical', ['node' => $node->id()]));
          }
        }
      }
    }

    // Breadcrumb needs to have terms cacheable metadata as a cacheable
    // dependency even though it is not shown in the breadcrumb because e.g. its
    // parent might have changed.
    $breadcrumb->addCacheableDependency($term);
    // @todo This overrides any other possible breadcrumb and is a pure
    //   hard-coded presumption. Make this behavior configurable per
    //   vocabulary or term.
    $parents = $this->termStorage->loadAllParents($term->id());
    // Remove current term being accessed.
    array_shift($parents);
    foreach (array_reverse($parents) as $term) {
      $term = $this->entityRepository->getTranslationFromContext($term);
      $breadcrumb->addCacheableDependency($term);
      $breadcrumb->addLink(Link::createFromRoute($term->getName(), 'entity.taxonomy_term.canonical', ['taxonomy_term' => $term->id()]));
    }

    // This breadcrumb builder is based on a route parameter, and hence it
    // depends on the 'route' cache context.
    $breadcrumb->addCacheContexts(['route']);

    return $breadcrumb;
  }

}

This class largely follows the breadcrumb construction logic provided by Drupal Core, and only adds a parent to the breadcrumb built according to a configuration parameter. This same logic can also be applied to the breadcrumb trail of content pages, in case you want a view for example in the breadcrumb trail, or any other page that is not a content page.

In the end, customizing a breadcrumb trail can be done in many ways, as is often the case with Drupal, but I must admit that finally the default pattern responds to many use cases and is very often sufficient with a configuration zest at the level of generating aliases of the pages of a Drupal 8 project. Finally, we can also note the Custom Menu Breadcrumbs module which allows us to configure a main parent from a menu item for content of a certain type.

 

Ajouter un commentaire