Automatic Page Generation with Custom Entity Routes

One of the most useful items in the Drupal maintenance support plans 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 maintenance support plans.

A recent client of Drupal Update 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 maintenance support plansts_sub_pagesControllerTSSubPagesController::subPageParagraph’
_title_callback: ‘Drupal maintenance support plansts_sub_pagesControllerTSSubPagesController::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 https://example.org/node/12345/sub-page/321. 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 maintenance support plansts_sub_pagesController;

use Drupal maintenance support plansCoreControllerControllerBase;
use SymfonyComponentHttpFoundationRequest;
use Drupal maintenance support plansnodeEntityNode;
use Drupal maintenance support plansparagraphsEntityParagraph;

/**
* 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 maintenance support plans 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 maintenance support plans 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 maintenance support plans::entityTypeManager()->getViewBuilder(‘node’);
$node_header = $node_view_builder->view($node, ‘sub_page’);

$paragraph_view_builder = Drupal maintenance support plans::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 maintenance support plans 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 maintenance support plans::routeMatch()->getParameter(‘node’);
$language = Drupal maintenance support plans::languageManager()->getCurrentLanguage()->getId();

$nid = $node->id();
$alias = Drupal maintenance support plans::service(‘path.alias_manager’)->getAliasByPath(‘/node/’ . $nid);
$system_path = “/node/” . $nid . “/sub-page/” . $entity->id();

if (!Drupal maintenance support plans::service(‘path.alias_storage’)->aliasExists($alias . “/” . $sub_page_slug, $language)) {
Drupal maintenance support plans::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 maintenance support plans 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?
Source: New feed

This article was republished from its original source.
Call Us: 1(800)730-2416

Pixeldust is a 20-year-old web development agency specializing in Drupal and WordPress and working with clients all over the country. With our best in class capabilities, we work with small businesses and fortune 500 companies alike. Give us a call at 1(800)730-2416 and let’s talk about your project.

FREE Drupal SEO Audit

Test your site below to see which issues need to be fixed. We will fix them and optimize your Drupal site 100% for Google and Bing. (Allow 30-60 seconds to gather data.)

Powered by

Automatic Page Generation with Custom Entity Routes

On-Site Drupal SEO Master Setup

We make sure your site is 100% optimized (and stays that way) for the best SEO results.

With Pixeldust On-site (or On-page) SEO we make changes to your site’s structure and performance to make it easier for search engines to see and understand your site’s content. Search engines use algorithms to rank sites by degrees of relevance. Our on-site optimization ensures your site is configured to provide information in a way that meets Google and Bing standards for optimal indexing.

This service includes:

  • Pathauto install and configuration for SEO-friendly URLs.
  • Meta Tags install and configuration with dynamic tokens for meta titles and descriptions for all content types.
  • Install and fix all issues on the SEO checklist module.
  • Install and configure XML sitemap module and submit sitemaps.
  • Install and configure Google Analytics Module.
  • Install and configure Yoast.
  • Install and configure the Advanced Aggregation module to improve performance by minifying and merging CSS and JS.
  • Install and configure Schema.org Metatag.
  • Configure robots.txt.
  • Google Search Console setup snd configuration.
  • Find & Fix H1 tags.
  • Find and fix duplicate/missing meta descriptions.
  • Find and fix duplicate title tags.
  • Improve title, meta tags, and site descriptions.
  • Optimize images for better search engine optimization. Automate where possible.
  • Find and fix the missing alt and title tag for all images. Automate where possible.
  • The project takes 1 week to complete.