Skip to content

Drupal how-to: list menu items, outputting a thumbnail image assigned to each item

This is a write-up of how I used menus and the .theme file in Drupal to create image galleries on landing pages. The post covers accessing the URL and alt text for each image, which is stored in a custom field.

Disclaimer: I’m not a Drupal expert and share this post only to help others who may have a similar use case. I make no guarantees of best practice! This should work in Drupal 8 onwards. Refinements welcome in the comments.

The scenario here is that you want to create a number of landing pages which, within the body of each landing page, list their immediate sub-pages. Along with a hyperlink, you also want to output a thumbnail image which has been assigned to each sub-page.

Say you were building a website for a professional wrestling organisation called the Scottish Wrestling Federation (SWF) and had a number of categories under a parent Roster page: Men, Women, Broadcast team and Referees. Each landing page is a gallery of photos which link to a relevant profile page.

First create a custom Image field called field_wrestler_photo (most of the roster are wrestlers so we’ll ignore the fact that a few are referees and announcers in terms of the naming). Then set up a Menu block called Sub-pages within the Main content region at /admin/structure/block. You should now see a theme hook suggestion at /admin/structure/block/manage/sub_pages#edit-settings-style (within the HTML and style options settings). Let’s name the block menu__sub_pages.

Next add a function called swf_preprocess_menu__sub_pages() to our swf.theme file. This function will process the URL and alt text data and assign it to properties of $variables. The data will then be available to us in menu--sub_pages.html.twig in the templates/navigation folder.

We’ll be using the Node interface within our preprocess function in swf.theme, so first include it towards the top of the file:

use Drupal\node\Entity\Node;

Begin our function by setting up a loop to get the data for each page:

function swf_preprocess_menu__sub_pages(&$variables) {
foreach ($variables['items'] as $key => $item) { // Loop through each page
// ...
}
}

For each page, get its URL (eg /roster/women/jean-meikle) and node path (eg /node/23). From the node path use preg_match() to get the node ID (23 in this example):

function swf_preprocess_menu__sub_pages(&$variables) {
foreach ($variables['items'] as $key => $item) {
$page_url = $variables['items'][$key]['url']->toString(); // Get page’s URL
$node_path = \Drupal::service('path_alias.manager')->getPathByAlias($page_url); // Get node path, eg /node/23
if (preg_match('/node\/(\d+)/', $node_path, $matches)) {
$node_id = intval($matches[1]); // Get the numeric node ID
// ...
}
}
}

Next, check for the property field_wrestler_photo and whether it has a value. Then get the Node object for this node ID:

function swf_preprocess_menu__sub_pages(&$variables) {
foreach ($variables['items'] as $key => $item) {
$page_url = $variables['items'][$key]['url']->toString();
$node_path = \Drupal::service('path_alias.manager')->getPathByAlias($page_url);
if (preg_match('/node\/(\d+)/', $node_path, $matches)) {
$node_id = intval($matches[1]);

if (
$node_id &&
Node::load($node_id)->field_wrestler_photo && // Has wrestler photo field
count(Node::load($node_id)->field_wrestler_photo) // Field has value
) {
$node = Node::load($node_id); // Get node object
// ...
}
}
}
}

You can now access the alt text, and use getFileUri() and the file_url_generator service to get the image’s URL. Finally, assign them to $variable properties so you can reference them in the Twig templates:

function swf_preprocess_menu__sub_pages(&$variables) {
foreach ($variables['items'] as $key => $item) {
$page_url = $variables['items'][$key]['url']->toString();
$node_path = \Drupal::service('path_alias.manager')->getPathByAlias($page_url);
if (preg_match('/node\/(\d+)/', $node_path, $matches)) {
$node_id = intval($matches[1]);

if (
$node_id &&
Node::load($node_id)->field_wrestler_photo &&
count(Node::load($node_id)->field_wrestler_photo)
) {
$node = Node::load($node_id);
$image_alt_text = $node->field_wrestler_photo->alt; // Get alt text
$file_uri = $node->field_wrestler_photo->entity->getFileUri(); // Get file URI
$image_url = \Drupal::service('file_url_generator')->generateString($file_uri); // Get image URL
$variables['items'][$key]['image_alt_text'] = $image_alt_text; // Store alt text
$variables['items'][$key]['image_url'] = $image_url; // Store image URL
}
}
}
}

Create a Twig template for the roster lists in templates/navigation/menu--sub_pages.html.twig:

{% import _self as menus %}

{{ menus.build_menu(items, attributes, 0) }}

{% macro build_menu(items, attributes, menu_level) %}
{% import _self as menus %}
{% if items %}
{% for item in items %}
{{ menus.add_link(item, attributes, menu_level) }}
{% endfor %}
{% endif %}
{% endmacro %}

{% macro add_link(item, attributes, menu_level) %}
<div>
<a href="{{ item.url }}">
<img loading="lazy" src="{{ item.image_url }}" alt="{{ item.image_alt_text }}" width="240" height="360"> {# Wrestler image #}
</a>
<div>{{ item.title }}</div> {# Wrestler name #}
</div>
{% endmacro %}

You can now create a page for each person in the roster and arrange them into categories by editing the menu struture at /admin/structure/menu/manage/main.

You probably want to restrict the sub-pages block to only appear on the Roster pages by adding relevant paths at /admin/structure/block/manage/sub_pages#edit-visibility-request-path: /roster, roster/men etc.

We’ve focused here on a custom image field, but you can access any default or custom field in a similar fashion. For example, you could add field_wrestler_height and field_wrestler_weight fields and list each wrestler’s height and weight under their photo.