Personnaliser le fil d'Ariane avec Drupal 8

Une route

Drupal 8 dispose par défaut de deux méthodes pour construire le fil d'Ariane. Pour les contenus, cette méthode est basé sur l'URL de la page, et pour les termes de taxonomie cette méthode est basée sur la hiérarchie du vocabulaire.

La construction par défaut d'un fil d'ariane

Explorons plus en détails la construction du fil d'Ariane pour les contenus.

Prenons un exemple d'une page de contenu dont l'URL est la suivante :

/services/freelance/drupal/usine-site-drupal

La dernière partie de l'URL (usine-site-drupal) correspond au titre de la page. Drupal va alors inspecter le reste de l'URL et pour chaque partie chercher si un contenu correspond à cette URL.

Ainsi, Drupal va inspecter cette URL, pour voir si elle correspond à un contenu existant.

/services/freelance/drupal

Dans l'affirmative (imaginons qu'un contenu dont le titre est Spécialiste Drupal a cette URL), le titre de la page est ajouté au fil d'Ariane.

Puis, il inspecte cette URL, pour voir si elle correspond à un contenu existant.

/services/freelance

Dans l'affirmative (le titre du contenu est Freelance Drupal par exemple), le titre de la page est ajouté au fil d'ariane.

Et enfin Drupal inspecte la dernière partie de l'URL, pour voir si elle correspond toujours à un contenu existant.

/services

Le titre de la page (Services) est alors ajouté au fil d'ariane.

Ainsi pour cet exemple, si chacune des parties du chemin correspond à une page de contenu existant, le fil d'ariane généré pour cette URL va être le suivant.

Accueil > Services > Freelance Drupal > Spécialiste Drupal > Usine à site Drupal

Il est ainsi possible de construire un fil d'ariane sur mesure, pertinent, en utilisant cette détection par chemin parent, en s'appuyant soit sur un alias manuel pour les pages de listing, les pages pivot ou landing pages, soit en s'appuyant sur le module Pathauto pour construire automatiquement un alias pertinent pour les contenus devant être placé automatiquement dans une section d'un site (exemple typique, les actualités, les événements, les services, etc.). 

A noter que la génération de la dernière partie du fil d'ariane, à savoir le titre de la page courante soit Usine à site Drupal dans notre exemple, est de la responsabilité du thème. En règle générale, vous trouverez une option dans tout thème correct qui vous permet d'afficher ou non le titre de la page courant dans le fil d'ariane. Ou encore cela peut être réalisé au moyen d'un 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
      );
    }
  }
}

Le fil d'ariane pour les pages des termes de taxonomie est quant à lui construit selon une logique différente : selon la hiérarchie des termes et ceci quelque soit l'alias utilisé pour les pages des termes.

Ces deux méthodes de génération d'un fil d'ariane sont les méthodes par défaut inclus dans Drupal Core. Il est possible bien entendu de modifier ce comportement par défaut au moyen de modules contribués, comme Menu Breadcrumb par exemple qui génère le fil d'ariane en fonction de la position de la page dans le menu principal et qui en l'absence de présence de la page dans le menu bascule sur la génération par défaut de Drupal Core, ou encore au moyen d'un module personnalisé.

Personnaliser le fil d'ariane au moyen d'un module

Altérer la construction du fil d'ariane se fait au moyen d'un service taggué avec le tag breadcrumb_builder. Par exemple

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 }

La priorité donnée à un service de ce type permet d'ordonner quelles règles appliquer en premier, les priorités les plus hautes étant celles appliquées en premier.

La Class MyModuleTermBreadcrumbBuilder doit implémenter deux méthodes

  • La méthode applies() qui va nous permettre d'indiquer quand appliquer cette règle de construction du fil d'ariane
  • La méthode build() qui va construire le fil d'ariane lui-même.

Ainsi si nous souhaitons par exemple ajouter un parent au niveau du fil d'ariane des pages de termes de taxonomies, notre class va ressembler à cela.

/**
 * 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;
  }

}

Cette classe reprend en grande partie la logique de construction du fil d'ariane fournie par Drupal Core, et ne fait que rajouter un parent au fil d'ariane construit selon un paramètre de configuration. Cette même logique peut aussi bien être appliquée au fil d'ariane des pages de contenus, dans le cas où par exemple vous souhaitez une vue par exemple dans le fil d'ariane, ou tout autre page qui n'est pas une page de contenu.

Au final, personnaliser un fil d'ariane peut être réalisé de multiples manières, comme souvent avec Drupal, mais je dois avouer que finalement le pattern par défaut répond à de nombreux cas d'usage et s'avère très souvent suffisant avec un zeste de configuration au niveau de la génération des alias des pages d'un projet Drupal 8. Pour conclure, nous pouvons noter également le module Custom Menu Breadcrumbs qui nous permet de configurer un parent principal depuis un élément de menu pour des contenus d'un certain type.

 

Ajouter un commentaire