ThinkShout: Automatic Page Generation with Custom Entity Routes

Planet Drupal - 9. Juli 2018 - 11:02

One of the most useful items in the Drupal 8 toolbox is the Paragraphs Module. By creating custom paragraph types, you can have much finer control over the admin and content creation process in Drupal.

A recent client of ThinkShout needed a content type (office locations) to include ‘sub-pages’ for things like office hours, services, and other items depending on the location. Most of the sub-page content was pretty simple, but they also needed to have direct links, be printable, and have the same header as the parent page. This ruled out an Ajax solution.

We’ve been using Paragraphs to make configurable content throughout the site, and since the sub-pages only have Title and Content fields, we thought they would be a good fit here as well. We then decided to explore the possibility of using custom entity routes to fulfill the other requirements.

To start, we created two additional view modes for the sub-page paragraphs called Sub-page and Menu link containing the Content and Title fields respectively. By keeping these fields in separate view modes, we make it much easier to work with them.

Next we created a custom module to hold all of our code, ts_sub_pages. In addition to the standard module files, we added the file ts_sub_pages.routing.yml, which contains the following:

ts_sub_pages.sub_pages: path: '/node/{node}/sub-page/{paragraph}' defaults: _controller: '\Drupal\ts_sub_pages\Controller\TSSubPagesController::subPageParagraph' _title_callback: '\Drupal\ts_sub_pages\Controller\TSSubPagesController::getTitle' options: parameters: node: type: entity:node paragraph: type: entity:paragraph requirements: _permission: 'access content'

This defines a unique system path based on the parent node ID and the paragraph entity ID. It would look like It also defines the call to the controller and the title_callback, essentially a location where we can create functions to manipulate the entity and its route. The options define the things to pass into the controller and title callback functions, and we also define access permissions using requirements.

One of the odd things about the controller and title_callback calls is that they look like a path, but are not. They have a predefined (and minimally documented) structure. You must do the following to make them work:

  • Create two folders in your module: src/Controller (case is important).
  • Create a file called TSSubPagesController.php - this must match the call.
  • Define a class matching TSSubPagesController in TSSubPagesController.php
  • Define a function matching subPageParagraph inside the TSSubPagesController class.

Example below. The names of the controller file, class, and function are up to you, but they must have the same case, and the file and class must match.

Digging into the TSSubPagesController.php file, we have a setup like so:

<?php namespace Drupal\ts_sub_pages\Controller; use Drupal\Core\Controller\ControllerBase; use Symfony\Component\HttpFoundation\Request; use Drupal\node\Entity\Node; use Drupal\paragraphs\Entity\Paragraph; /** * TS Sub Pages controller. */ class TSSubPagesController extends ControllerBase { /** * {@inheritdoc} */ public function subPageParagraph(Paragraph $paragraph, Node $node, Request $request) {

Here we have the namespace - this is our module. Note again that the src is taken for granted. Next are the Symfony/Drupal use statements, to pull in the classes/interfaces/traits we’ll need. Then we extend the ControllerBase class with TSSubPagesController, and define our subPageParagraph function. The function pulls in the $node and $paragraph options we defined in ts_sub_pages.routing.yml.

Now we can finally get to work on our sub-pages! Our goal here is to bring in the parent node header fields on every sub-page path. In the Drupal admin interface, go to ‘Manage Display’ for your content type. In our case it was /admin/structure/types/manage/location/display. Scroll to the bottom and under ‘Custom display settings’ you’ll find a link to ‘Manage view modes’. We added a mode called sub-page, and added all of the fields from our Location’s header.

Now we can bring that view of the node into the sub-page using the subPageParagraph function we defined above:

<?php public function subPageParagraph(Paragraph $paragraph, Node $node, Request $request) { $node_view_builder = \Drupal::entityTypeManager()->getViewBuilder('node'); $node_header = $node_view_builder->view($node, 'sub_page'); $paragraph_view_builder = \Drupal::entityTypeManager()->getViewBuilder('paragraph'); $paragraph_body = $paragraph_view_builder->view($paragraph, 'sub_page'); return ['node' => $node_header, 'paragraph' => $paragraph_body]; }

We get the node and paragraphs using getViewBuilder, then the view modes for each. The node’s ‘sub-page’ view mode contains all of the header fields for the node, and the paragraph ‘sub-page’ view mode contains the paragraph body. We return these, and the result is what looks like a page when we visit the base paragraph url of /node/12345/sub-page/321. The title is missing though, so we can add that with another small function inside the TSSubPagesController class (we call it using the _title_callback in ts_sub_pages.routing.yml):

<?php /** * Returns a page title. */ public function getTitle(Paragraph $paragraph, Node $node) { $node_title = $node->getTitle(); $paragraph_title = $paragraph->field_title_text->value; return $node_title . ' - ' . $paragraph_title; }

Now we need to build a menu for our sub-pages. For this we can just use the ‘sub-pages’ paragraph field on the parent node. In the admin display, this field is how we add the sub-page paragraphs, but in the public-facing display, we use it to build the menu.

First, make sure you include it in the ‘default’ and ‘sub-page’ displays as a Rendered Entity, using the “Rendered as Entity” Formatter, which has widget configuration where you need to select the “Menu Link” view mode. When we set up the Paragraph, we put the Title field in the ‘Menu Link’ view. Now the field will display the titles of all the node’s sub-pages. To make them functional links, go to the ‘Menu Link’ view mode for your sub-page paragraph type, make the Title a ‘Linked Field’, and use the following widget configuration:

Destination: /node/[paragraph:parent_id]/sub-page/[paragraph:id] Title: [paragraph:field_title_text]

Next we need to account for the fact that the site uses URL aliases. A node called ‘main office’ will get a link such as /locations/main-office via the Pathauto module. We want our sub-pages to use that path.

We do this by adding a URL Alias to the sub-page routes on creation (insert) or edit (update). In our module, we add the following functions to the ts_sub_pages.module:

<?php /** * Implements hook_entity_insert(). */ function ts_sub_pages_entity_insert(EntityInterface $entity) { if ($entity->getEntityTypeId() == 'paragraph' && $entity->getType() == "custom_subpage") { _ts_sub_pages_path_alias($entity); } } /** * Implements hook_entity_update(). */ function ts_sub_pages_entity_update(EntityInterface $entity) { if ($entity->getEntityTypeId() == 'paragraph' && $entity->getType() == "custom_subpage") { _ts_sub_pages_path_alias($entity); } }

These get called every time we add or update the parent node. They call a custom function we define just below. It’s important to note that we have a custom title field field_title_text defined - your title may be the Drupal default:

<?php /** * Custom function to create a sub-path alias. */ function _ts_sub_pages_path_alias($entity) { $sub_page_slug = Html::cleanCssIdentifier(strtolower($entity->field_title_text->value)); $node = \Drupal::routeMatch()->getParameter('node'); $language = \Drupal::languageManager()->getCurrentLanguage()->getId(); $nid = $node->id(); $alias = \Drupal::service('path.alias_manager')->getAliasByPath('/node/' . $nid); $system_path = "/node/" . $nid . "/sub-page/" . $entity->id(); if (!\Drupal::service('path.alias_storage')->aliasExists($alias . "/" . $sub_page_slug, $language)) { \Drupal::service('path.alias_storage') ->save($system_path, $alias . "/" . $sub_page_slug, $language); } }

This function gets the sub-page paragraph title, and creates a URL-friendly slug. It then loads the paragraph’s node, gets the current language, ID, and alias. We also build the system path of the sub-page, as that’s necessary for the url_alias table in the Drupal database. Finally, we check that there’s no existing path that matches ours, and add it. This will leave old URL aliases, so if someone had bookmarked a sub-page and the name changes, it will still go to the correct sub-page.

Now we can add the ‘Home’ link and indicate when a sub-page is active. For that we’ll use a custom twig template. The field.html.twig default file is the starting point, it’s located in core/themes/classy/templates/field/. Copy and rename it to your theme’s template directory. Based on the field name, this can be called field--field-sub-pages.html.twig.

The part of the twig file we’re interested in is here:

{% for item in items %} <div{{ item.attributes.addClass('field__item') }}>{{ item.content }}</div> {% endfor %}

This occurs three times in the template, to account for multiple fields, labels, etc. Just before each of the for loops, we add the following ‘home’ link code:

{% if url('<current>')['#markup'] ends with node_path %} <div class="field__item active" tabindex="0">Home</div> {% else %} <div class="field__item"><a href="{{ node_path }}">Home</a></div> {% endif %}

Next, we make some substantial changes to the loop:

{% set sub_text = item.content['#paragraph'].field_title_text.0.value %} {% set sub_path = node_path ~ '/' ~ sub_text|clean_class %} {% if url('<current>')['#markup'] ends with sub_path %} <li{{ item.attributes.addClass('field__item', 'menu-item', 'active') }}>{{ sub_text }}</li> {% else %} <li{{ item.attributes.addClass('field__item', 'menu-item') }}><a href="{{ sub_path }}">{{ sub_text }}</a></li>

Here, sub_text gets the sub-page title, and sub_path the path of each sub-page. We then check if the current url ends with the path, and if so, we add the active class and remove the link.

And that’s it! The client can now add as many custom sub-pages as they like. They’ll always pick up the parent node’s base path so they will be printable, direct links. They’ll have the same header as the parent node, and they will automatically be added or deleted from the node’s custom context-aware menu.

Hmm, maybe this would make a good contributed module?

Elektro-Fahrräder: Fahrradbranche lehnt generelle Versicherungspflicht ab

heise online Newsticker - 9. Juli 2018 - 10:30
Die EU-Kommission plant, die Versicherungspflicht auch auf Pedelecs auszuweiten. Die Fahrradbranche befürchtet Verluste bei E-Bikes und kündigt Widerstand an.

Zwei Jahre Pokémon Go – und kein Ende abzusehen

heise online Newsticker - 9. Juli 2018 - 10:30
Zwei Jahre nach dem Start spielen immer noch über fünf Millionen täglich Pokémon Go. Das ist auch gezieltem Taktieren zu verdanken.

Samsung verdient weniger als erwartet, Smartphone-Geschäft schwächelt

heise online Newsticker - 9. Juli 2018 - 10:00
Der Gewinn beim Smartphone-Marktführer fällt etwas geringer aus als zuvor und als erwartet. Am meisten Geld verdient Samsung mit seinem starken Chip-Geschäft.

Test Dell XPS 15 2-in-1: Notebook mit Vega-Grafik und "Magnetschwebetastatur"

heise online Newsticker - 9. Juli 2018 - 9:30
Das Dell XPS 15 2-in-1 wandelt zwischen Notebook und Tablet. Angetrieben wird es vom Core i7-8705G, der Intel-Prozessor und AMD-Grafik vereint.

Selbstlernender Algorithmus beherrscht Rubik's Cube

heise online Newsticker - 9. Juli 2018 - 9:00
Forscher an der University of California haben eine Software entwickelt, die sich selbst beibringt, den Zauberwürfel zu knacken.

Open Photonik Pro: Neues Förderprogramm für Maker und Gründer

heise online Newsticker - 9. Juli 2018 - 9:00
Das Forschungsministerium will neue Photonikprodukte fördern und hat dafür ein Programm aufgelegt, das auf die Zusammenarbeit von Makern und Firmen setzt.

Bikesharing: Obike ist offenbar pleite

heise online Newsticker - 9. Juli 2018 - 9:00
In Singapur hat ein vorläufiger Insolvenzverwalter die Regie beim Bikesharing-Anbieter Obike übernommen. Tausende Kunden warten auf ihre Kaution.

"Todesdrohungen": Klagen über Lobbying überschatten EU-Copyright-Entscheid

heise online Newsticker - 9. Juli 2018 - 8:30
Nach der vom EU-Parlament beschlossenen Pause der Verhandlungen über die Copyright-Reform liegen bei Abgeordneten und Lobbyisten die Nerven noch immer blank.

Cross-Plattform-Entwicklung: Flutter als Preview verfügbar

heise online Newsticker - 9. Juli 2018 - 8:00
Die neuste Version des Frameworks für Android- und iOS-Apps bietet einige Fehlerbereinigungen und verbesserte Stabilität.

Kaufberatung: Alle Mods für die Moto-Z-Smartphones

heise online Newsticker - 9. Juli 2018 - 7:30
Der Verkaufsstart des neuen Moto Z3 Play steht kurz bevor. Ein Grund mehr, sich nochmal alle Erweiterungen für das modulare Smartphone anzusehen.

NSO-Mitarbeiter bietet iOS-Spyware Pegasus im Darknet an

heise online Newsticker - 9. Juli 2018 - 7:00
Der geheimnisumwitterten israelischen Sicherheitsfirma NSO Group sind mächtige Spyware-Tools abhanden gekommen. Ein Insider wollte sie im Darknet verkaufen.

Hackerangriff auf Gentoo: Entwickler nutzen jetzt Zweifaktor-Anmeldung

heise online Newsticker - 8. Juli 2018 - 17:30
Die Entwickler der Linux-Distribution Gentoo haben ihren Abschlussbericht über den Hackerangriff vorgelegt, der sich vorige Woche ereignete.

Hate Speech im Netz: Viel Hass, wenig Hetzer

heise online Newsticker - 8. Juli 2018 - 17:00
Hassrede ist laut einer Umfrage im Internet für mehr Menschen als bisher sichtbar geworden.

Drupixels: Enable debug mode and error reporting for local development in Drupal 8

Planet Drupal - 8. Juli 2018 - 16:57
If you are just starting with Drupal 8 then one of the most important things you should know is to enable the debug mode and error reporting on Drupal 8. This is really important for backend as well as frontend developer to the full error on your screen while you are working because you might not know what is the exact issue with the site with just a generic error statement.

Sommerzeit ja oder nein? – EU-Kommission befragt Bürger online

heise online Newsticker - 8. Juli 2018 - 16:00
Immer wieder werden Forderungen laut, die halbjährliche Zeitumstellung abzuschaffen. Jetzt will die EU-Kommission die Meinungen der Bürger wissen.

Israelische Soldaten über WM-Apps aus dem Play Store gehackt

heise online Newsticker - 8. Juli 2018 - 14:00
Knapp 100 Soldaten der israelischen Armee wurden Opfer von Hackerangriffen, die der Hamas zugeschrieben werden.

Wir wollen Ihren Vortrag für die c't <webdev> 2019

heise online Newsticker - 8. Juli 2018 - 13:30
Ab sofort können sich Referenten für einen Vortrag oder Workshop bei der Frontend-Konferenz bewerben. Sie findet vom 6. bis 8. Februar 2019 statt.

25 Jahre Hunde im Internet – ein Cartoon erklärt das Netz

heise online Newsticker - 8. Juli 2018 - 12:00
"Im Internet weiß niemand, dass du ein Hund bist": Mit diesem Cartoon zeigte der New Yorker vor 25 Jahren, dass das Netz Mainstream geworden war.

Maroder Beton: Neue Probleme im belgischem AKW Tihange

heise online Newsticker - 8. Juli 2018 - 12:00
Während der Reparatur maroder Betonteile fiel auf, dass Stahlverstärkungen an einer Schutzdecke nicht den Bauplänen entsprechen.