Illustration: Hänsel and Gretel av Alexander Zick

Breadcrumbs i Drupal 8

Detta inlägg kommer anta att du som läsare har grundläggande kunskaper inom Drupal, PHP, Objekt-orienterad programmering samt en del terminologi som är vanligt förekommande när man utvecklar moderna webbplatser.
 

Hanteringen av breadcrumbs i Drupal har länge varit ökänt svårbegriplig. Problemet har varit att det inte funnits något tydligt system för hur breadcrumbs ska definieras, det finns därför många olika lösningar att ta till och många moduler som kan lösa problemet på sitt eget lilla vis. Oftast är det ingen av lösningarna som faktiskt ger ett önskvärt resultat.

Men inte längre

I Drupal 8 har man tänkt om helt när det gäller breadcrumbs och introducerat ett helt nytt system för att definiera dessa genom kod, det ger en otrolig frihet till hur man kan formatera det slutgiltiga resultatet. Detta betyder att breadcrumbs inte längre byggs upp automatiskt genom att Drupal försöker lista ut hur den ska se ut utifrån vilka menyalternativ som är aktiva.

Placera var du vill

Tidigare var man tvungen att skriva ut breadcrumbs via temat då det fanns en "$breadcrumb"-variabel. Den har i Drupal 8 ersatts med ett block så att det nu går att placera breadcrumbs var man vill. Dessutom går det att skriva ut den flera gånger på samma sida då även block har blivit betydligt bättre och kraftfullare i Drupal 8.

Hur fungerar breadcrumbs i Drupal 8?

För att kunna bygga sina egna breadcrumbs i Drupal 8 måste man först ha en egen modul där man kan registrera sina breadcrumbs. När man bygger breadcrumbs i Drupal 8 gör man det via en s.k. "Breadcrumb Builder".

Det första som måste skapas för att vi ska kunna börja bygga breadcrumbs är en klass, i exemplen nedan kommer vi skapa en breadcrumb builder för artiklar. Klassen lägger vi i en modul som heter example_breadcrumbs och under namespace Drupal\example_breadcrumbs\Breadcrumb, vi döper filen till ArticleBreadcrumbBuilder.php.

Vår breadcrumb builder måste även implementera ett interface vid namn BreadcrumbBuilderInterface, detta interface kommer beskriva vilka metoder vår klass måste implementera.

Du kan lägga din breadcrumb builder i någon modul du skapat själv och under ett namespace du själv tycker fungerar bra för dig, det spelar alltså ingen roll vart du lägger den så länge ditt namespace stämmer och din modul är installerad.

Vår slutgiltiga kod kommer att se ut som nedan. Vad den gör är att den bygger upp en breadcrumb som består av följande delar:

  • Home (Länk till förstasidan)
  • Tag (Länk till första termen artikeln är taggad till)
  • Titel (Titel på artikel utan länk)

<?php
namespace Drupal\example_breadcrumbs\Breadcrumb;

use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\NodeInterface;

class ArticleBreadcrumbBuilder implements BreadcrumbBuilderInterface {

  use StringTranslationTrait;

  /**
   * {@inheritdoc}
   */
  public function applies(RouteMatchInterface $route_match) {
    $node = $route_match->getParameter('node');

    /** @var NodeInterface $node */
    if ($node && $node instanceof NodeInterface) {
      return $node->getType() === 'article';
    }
  }

  /**
   * {@inheritdoc}
   */
  public function build(RouteMatchInterface $route_match) {
    $breadcrumb = new Breadcrumb();

    // Add the current route as a cache context.
    $breadcrumb->addCacheContexts(['route']);

    /** @var NodeInterface $node */
    $node = $route_match->getParameter('node');

    // Add a home link.
    $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));

    // Add the first tag as a breadcrumb if the article has any tags.
    if ($node->hasField('field_tags')) {
      $tags = $node->get('field_tags');

      if (count($tags)) {
        $term = \Drupal::entityTypeManager()
          ->getStorage('taxonomy_term')
          ->load($tags->first()->getValue()['target_id']);

        $breadcrumb->addLink($term->toLink());
      }
    }

    // Add the node title, will not link anywhere.
    $breadcrumb->addLink(Link::createFromRoute($node->label(), '<none>'));

    // Add the node as a cacheable dependency.
    $breadcrumb->addCacheableDependency($node);

    return $breadcrumb;
  }
}
?>

Var del för sig

För att du bättre ska förstå vad de olika delarna av koden gör kommer vi gå igenom den del för del och titta på vad det är vi gör och vad som händer:

Ska jag appliceras?

Den första metoden i vår klass är den som bestämmer om just denna breadcrumb builder ska appliceras på sidan vi besöker, i vårt exempel tittar vi på informationen från den route som matchade förfrågan som gjordes och ser ifall det finns en parameter vid namn node.

Ifall denna parameter finns ser vi till att det är en instans av NodeInterface och ser samtidigt till att noden är av typ "article". Det gör att vår breadcrumb builder endast appliceras på noder av typen article.

<?php
public function applies(RouteMatchInterface $route_match) {
  $node = $route_match->getParameter('node');

  /** @var NodeInterface $node */
  if ($node && $node instanceof NodeInterface) {
    return $node->getType() === 'article';
  }
}
?>

Bygga våra faktiska breadcrumbs

Om logiken som kollar om vår breadcrumb builder ska användas returnerar "sant" så kommer Drupal köra metoden build, det är här vi bygger våra faktiska breadcrumbs.

Utan några länkar i våra breadcrumbs hade det sett ut som följande exempel. Det enda vi gör här är egentligen att skapa en ny instans av klassen Breadcrumb och returnera den. En breadcrumb builder måste alltid returnera ett objekt av denna klassen.

<?php
public function build(RouteMatchInterface $route_match) {
  $breadcrumb = new Breadcrumb();
  return $breadcrumb;
}
?>

Men en breadcrumb är ju inget utan sina länkar så den första länken vi lägger till är en länk som låter en komma tillbaka till framsidan. När vi lägger till länkar använder vi metoden addLink som finns tillgänglig på Breadcrumb-klassen. Den här metoden förväntar sig ett objekt av typen Link.

<?php
public function build(RouteMatchInterface $route_match) {
  $breadcrumb = new Breadcrumb();
  
  $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
  
  return $breadcrumb;
}
?>

För att lägga till nästa länk som är den första taggen på artikeln kollar vi först så att fältet field_tags finns på denna noden. Vi skulle kunna anta att det finns men om det av någon anledning skulle tas bort fungerar det fortfarande. Nedan ser du koden efter att vi har lagt till länken i våra breadcrumbs. Noden finns tillgänglig genom att hämta parametern node från route match.

<?php
public function build(RouteMatchInterface $route_match) {
  $breadcrumb = new Breadcrumb();
  
  /** @var NodeInterface $node */
  $node = $route_match->getParameter('node');
  
  $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));
  
  if ($node->hasField('field_tags')) {
    $tags = $node->get('field_tags');

    if (count($tags)) {
      $term = \Drupal::entityTypeManager()
        ->getStorage('taxonomy_term')
        ->load($tags->first()->getValue()['target_id']);

      $breadcrumb->addLink($term->toLink());
    }
  }
  
  return $breadcrumb;
}
?>

Sista länken vi lägger till är titeln på vår artikel, vi vill inte länka den någonstans så vi kommer enbart att ange en titel på länken och sätta länkens mål till "<none>", detta gör att länken visas som text utan att formateras som en länk. Efter att vi har lagt till länken kommer vår kod se ut enligt följande:

<?php
public function build(RouteMatchInterface $route_match) {
  $breadcrumb = new Breadcrumb();

  /** @var NodeInterface $node */
  $node = $route_match->getParameter('node');

  $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));

  if ($node->hasField('field_tags')) {
    $tags = $node->get('field_tags');
  
    if (count($tags)) {
      $term = \Drupal::entityTypeManager()
        ->getStorage('taxonomy_term')
        ->load($tags->first()->getValue()['target_id']);
  
      $breadcrumb->addLink($term->toLink());
    }
  }

  $breadcrumb->addLink(Link::createFromRoute($node->label(), '<none>'));

  return $breadcrumb;
}
?>

Nu är vi nästan klara med vår breadcrumb builder, det sista vi måsta lägga till är konfiguration för hur cache ska hanteras i våra breadcrumbs, det är väldigt viktigt att vi inte missar detta då vi kan få väldigt oväntade resultat annars. Exempelvis kan vi få samma breadcrumbs på alla sidor av sajten även där vi inte förväntar oss några alls.

Det första vi måste lägga till är följande kod, detta säger till Drupal att cache för aktuella breadcrumbs är specifika för den nuvarande routen:

<?php
$breadcrumb->addCacheContexts(['route']);
?>

Vi måste även lägga till noden som en del av cache för denna breadcrumb, det gör så att cache för breadcrumbs kommer ogiltigförklaras varje gång noden uppdateras vilket i sin tur meddelar för Drupal att våra breadcrumbs behöver byggas om:

<?php
$breadcrumb->addCacheableDependency($node);
?>

Sist men inte minst

För att Drupal ska bygga upp breadcrumbs måste vår bredcrumb builder registreras genom "*.services.yml". I den filen kommer vi att fylla i något liknande exemplet nedan där vi byter ut modulens namn, namespace samt filnamn om vi väljer att döpa dem annorlunda. Efter att vi lagt till följande måste vi även rensa cache för att Drupal ska läsa in ändringarna i vår "*.services.yml" fil.

Väldigt viktigt här är tags och vår tag med namnet breadcrumb_builder, den berättar för Drupal vilken klass som ska användas när breadcrumbs byggs för sidan:

services:
  example_breadcrumbs.breadcrumb_article:
    class: Drupal\example_breadcrumbs\Breadcrumb\ArticleBreadcrumbBuilder
    tags:
      - { name: breadcrumb_builder, priority: 100 }

Med detta har vi nu en fungerande breadcrumb builder för artiklar som skriver ut breadcrumbs där det första värdet är en länk till framsidan, den andra länken är första taggen om det finns taggar och den sista länken - som visas enbart som text - är artikelns titel.