I am trying to create next/previous page links like the Book module.

I have two content types, Chapter and Page.

The Chapter content type contains an entity reference field (unlimited) for pages.

So, each chapter contains an arbitrary number of pages in the entity reference field Pages in Chapter, and these are re-ordered to determine the order of the pages.

Now, on each Page, I want to make links to the previous and next pages based on the values of the parent Chapter's entity reference field Pages in Chapter.

Specifically, using a view or block or similar on the Page, I need to get the Chapter that references the current Page, iterate through the values of the entity reference field Pages in Chapter, find the Page that is immediately before and the Page that is immediately after the current Page, and then output this as a simple unordered list.

I can code a little but I'm not very good so I'm having trouble finding an approach to implement this.

What I tried

I tried to build this in Views. The problem I encountered was that it is very difficult to iterate through the subdivision entity reference field and filter out the previous and next links.

This is a similar question for Drupal 7 but since Drupal 8 includes so much more in core (Views and Entity References, for example), I think it's fair to ask the question again.

Why not just use the Book module?

I only need the next/previous page functionality, and ideally it would be based on my entity reference field Pages in Chapter. The book module navigation in Drupal 8 doesn't follow the same patterns as the rest of core (it's not a block, and it's not a menu). I spent a couple hours this afternoon going through the Book issue queue and cleaning up old issues, and it seemed there is very little effort to "modernize" the module, so I thought it would be better to try to implement this feature on my own.

  • I haven't tried it but I think you should be able to use Views to create this. Since Chapter with you is the Child, Pages are Parents you need to create a list of Aunts and Uncles? Usually this type of setup is done by structuring the Pages to be Children, and Chapters Parents so you're looking to list the siblings. There are answers/tutorials about listing siblings, maybe that can push you in the right direction. After you have the list of correct Aunts/Uncles you use the ER field weight property to get the order, you might need a Relationship to pull that in. And figure out which are closest – prkos May 23 at 13:44
  • Have you considered using a menu for each chapter? or, even better, a single menu with multiple levels where level 1 = chapter and level 2 = page? Menus are great in that they automatically handle the 'active trail' (showing which page you're currently on) and there are heaps of contrib modules around menus. I suspect this is why the 'book' module has become relatively defunct. – Darvanen May 26 at 22:45
  • @Darvanen I asked about approaches to menus in this question: drupal.stackexchange.com/questions/293960/… – Patrick Kenny May 27 at 1:17
  • In that case yeah, NoSssweat's answer is a decent one. – Darvanen May 28 at 2:07

I can code a little but I'm not very good so I'm having trouble finding an approach to implement this.

I started 7 years ago from zero, I only knew HTML and CSS. I learned and mastered Rules & Views. But like 4 or 5 years ago, I ran into scenarios like this one where the existing modules didn't quite do what I needed or felt I had to do overkill/bulky-overhead workarounds with existing modules. So I learned PHP from online resources and I learned how to create custom modules for Drupal.

I highly suggest you improve your backend/coding game. Specially since Rules in D8 is barebones compared to D7.

I tried to build this in Views. The problem I encountered was that it is very difficult to iterate through the subdivision entity reference field and filter out the previous and next links.

While it's possible to programmatically solve it with Views, it feels overkill/unnecessary to do this in Views just for prev and next links. A better approach is to create prev_url and next_url variables for Twig. Using a preprocess function we can create and pass those variables to the appropriate template file, node.html.twig in this case.

I've put lots of comments, so you can follow and hopefully learn a thing or two.

In mytheme.theme (since this particular hook can also be used in theme, rather than creating a custom module).

 * Implements hook_preprocess_HOOK() for node.html.twig.
function mytheme_preprocess_node(&$variables) {
  $node = $variables['elements']['#node']; // obtain current page node object
  if ($node->getType() == 'page') { // check it's a "page" content type
    $page_nid = $node->id(); // get the current page node id
    // get Chapter nids using Entity Field Query
    $query = \Drupal::entityQuery('node')
      ->condition('type', 'chapter')
      ->condition('field_reference', $page_nid); // check if current page NID is set on any Chapter's reference field
    $chapter_nids = $query->execute(); // returns array of Chapter nids
    if (!empty($chapter_nids)) { // check we have a result
      $chapter_nid = reset($chapter_nids); // Extracts the first array value, since we assume current page can only belong to one Chapter.
      $chapter_node = Drupal\node\Entity\Node::load($chapter_nid); // load chapter node
      $referenced_page_nodes = $chapter_node->field_reference; // get referenced nodes
      foreach ($referenced_page_nodes as $key => $referenced_item) { // loop though referenced nodes
        if ($referenced_item->entity) {
          if ($node->id() == $referenced_item->entity->id()) { // check if current page nid equals referenced nid
            $prev_key = $key - 1; // we'll use these keys to access prev/next array values
            $next_key = $key + 1;
            if (!empty($referenced_page_nodes[$prev_key])) { // check if previous key exists
              $prev_url = $referenced_page_nodes[$prev_key]->entity->toUrl()->toString(); // get prev URL
              $variables['prev_url'] = $prev_url; // creates prev_url variable in node.html.twig
            if (!empty($referenced_page_nodes[$next_key])) { // check if next key exists
              $next_url = $referenced_page_nodes[$next_key]->entity->toUrl()->toString(); // get next URL
              $variables['next_url'] = $next_url; // creates next_url variable in node.html.twig
            break; // since we found it, exit from loop as no point in continuing to loop through.

Note: Rename field_reference (2 places) to the machine name of your actual reference field in Chapter content type.

In mytheme/templates/node.html.twig, you can place them after the {{ content }} part

{% if prev_url is not empty %}
  <span><a href="{{ prev_url }}">{{ 'Previous page'|t }}</a></span>
{% endif %}
{% if next_url is not empty %}
  <span><a href="{{ next_url }}">{{ 'Next page'|t }}</a></span>
{% endif %}
| improve this answer | |
  • To avoid making links to unpublished nodes, which will result in an access denied error and expose the unpublished URL, I added if (!empty($referenced_page_nodes[$prev_key]) && $referenced_page_nodes[$prev_key]->entity->isPublished()) and if (!empty($referenced_page_nodes[$next_key]) && $referenced_page_nodes[$next_key]->entity->isPublished()) {. – Patrick Kenny May 27 at 6:22
  • @PatrickKenny if you have say 1, 2, 3, 4, 5 and 3 is unpublished, with your current code, 2 won't link to 3, but it also won't link to 4 or perhaps 5. You might want to make it link to 4 or 5, whichever is the next published node, if there is one. – No Sssweat May 27 at 8:03
  • Where could I move this code so that I could move it around in Layout Builder? Just a brief suggestion of the general direction would be very helpful. – Patrick Kenny Jun 28 at 16:34
  • 1
    @PatrickKenny custom block or View block. If you go with custom block, you'll need to add cache context to it as you want the block to be dynamic based on page. – No Sssweat Jun 28 at 18:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.