Displaying Detail Pages in the Contao Navigation

The Contao navigation frontend module only outputs pages that exist in the page tree as navigation items, e.g. /products/overview. If you also want to show detail pages such as /product/detail/item-number/1364 in addition to regular Contao pages — even though only the page /product/detail exists in the page tree — there are several approaches.

Custom Pages in the Page Tree with Alias for Redirect

If the page /product/detail exists as a Contao page where the desired article is shown via the slug parameters item-number/1364, you can add a new page in the page tree at the desired position for the navigation, e.g. with the title “Article 1364”. However, the alias of that page is set manually to the alias of the detail view /product/detail/item-number/1364. For the “Article 1364” navigation link to also display the desired content, the page /product/detail must have a higher route priority (10) than the page item-number/1364 (0).

More tips on route priority.

ParseTemplateListener for Customising the Navigation

With the ParseTemplateListener, the template nav_default (or custom variants of it) can still be manipulated before it is “delivered”. This makes it possible to add custom navigation links at any desired position (see getSublinks()). The following is example code:

  1<?php
  2// src/EventListener/ParseTemplateListener.php
  3
  4namespace App\EventListener;
  5
  6use Contao\CoreBundle\DependencyInjection\Attribute\AsHook;
  7use Contao\Template;
  8use MetaModels\Filter\Setting\IFilterSettingFactory;
  9use MetaModels\IFactory;
 10use MetaModels\IMetaModel;
 11use MetaModels\Render\Setting\IRenderSettingFactory;
 12use Symfony\Component\HttpFoundation\RequestStack;
 13
 14use function sprintf;
 15use function str_replace;
 16use function trim;
 17
 18#[AsHook('parseTemplate')]
 19class ParseTemplateListener
 20{
 21    public function __construct(
 22        private readonly IFactory $factory,
 23        private readonly IFilterSettingFactory $filterFactory,
 24        private readonly IRenderSettingFactory $renderFactory,
 25        private readonly RequestStack $requestStack,
 26    ) {
 27    }
 28
 29    public function __invoke(Template $template): void
 30    {
 31        if ('nav_default' === $template->getName()) {
 32            $levelData = $template->getData();
 33
 34            // Check only level 1.
 35            if ('level_1' !== $levelData['level']) {
 36                return;
 37            }
 38
 39            $items = $levelData['items'];
 40            foreach ($items as &$item) {
 41                // Check only page id 7.
 42                if (7 !== ($item['id'] ?? null)) {
 43                    continue;
 44                }
 45
 46                // Add subitems as level 2 at page id 7 and mark parent as trail.
 47                if ([] !== ($subLinks = $this->getSublinks())) {
 48                    $item['subitems'] = $subLinks['subitems'];
 49                    $item['class']    =
 50                        trim(
 51                            'submenu ' . ($subLinks['trail'] ? str_replace('sibling', 'trail', $item['class']) : '')
 52                        );
 53                }
 54            }
 55            unset($item);
 56            $levelData['items'] = $items;
 57
 58            $template->setData($levelData);
 59        }
 60    }
 61
 62    private function getSublinks(): array
 63    {
 64        // Begin configuration.
 65        $modelName = 'mm_employees';
 66        $renderId  = 4;
 67        $filterId  = 3;
 68        // End configuration.
 69
 70        if (!(($model = $this->factory->getMetaModel($modelName)) instanceof IMetaModel)) {
 71            return [];
 72        }
 73
 74        $filter           = $model->getEmptyFilter();
 75        $filterCollection = $this->filterFactory->createCollection($filterId);
 76        $filterCollection->addRules($filter, []);
 77        $items = $model->findByFilter($filter);
 78
 79        if (!$items->getCount()) {
 80            return [];
 81        }
 82
 83        $parsed = $items->parseAll('text', $this->renderFactory->createCollection($model, $renderId));
 84        unset($items, $filterCollection, $filter, $model);
 85
 86        $request = $this->requestStack->getCurrentRequest();
 87        if (null === $request) {
 88            return [];
 89        }
 90        $path        = $request->getRequestUri();
 91        $isTrail     = false;
 92        $subLinkList = '<ul class="level_2">';
 93        foreach ($parsed as $item) {
 94            $href = $item['actions']['jumpTo']['href'];
 95            // Possibly clean up the path+href from GET parameters or anchor links.
 96            if ($path !== $href) {
 97                $subLinkList .= sprintf(
 98                    '<li><a href="%1$s" title="%2$s">%2$s</a></li>',
 99                    $href,
100                    $item['text']['name']
101                );
102            } else {
103                $isTrail     = true;
104                $subLinkList .= sprintf(
105                    '<li class="active"><strong class="active" aria-current="page">%s</strong></li>',
106                    $item['text']['name']
107                );
108            }
109        }
110        $subLinkList .= '</ul>';
111
112        return ['trail' => $isTrail, 'subitems' => $subLinkList];
113    }
114}

More on registering services in the linked article.

Extension “hofff/contao-navigation” and “TreeEvent”

The extension “Contao-Navigation” provides its own frontend module for navigation. It also has various events that allow more elegant output manipulation compared to the ParseTemplateListener. The following is example code for appending an additional link in the navigation — this can also be adapted for outputting MM detail pages.

 1<?php
 2// src/EventListener/NavigationMenuListener.php
 3
 4namespace App\EventListener;
 5
 6use Hofff\Contao\Navigation\Event\TreeEvent;
 7use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
 8
 9use function array_keys;
10
11#[AsEventListener('Hofff\Contao\Navigation\Event\TreeEvent')]
12class NavigationMenuListener
13{
14    public function __invoke(TreeEvent $treeEvent): void
15    {
16        $moduleId  = $treeEvent->moduleModel()->id; // Module id for checking if it's the correct module.
17        $pageId    = $treeEvent->items()->currentPage->id; // Page id for checking if it's the correct page.
18        $pageItems = $treeEvent->items(); // Get the page items for the navigation tree.
19        $rootIds   = $pageItems->roots;
20
21        // Add a new item to the first root as the last one.
22        $pageItems->subItems[array_keys($rootIds)[0]][] = 9999;
23
24        // Item data.
25        $pageItems->items[9999] = [
26            'class'     => 'mm-page',
27            'isInTrail' => false,
28            'isActive'  => false,
29            'pageTitle' => 'MetaModels',
30            'accesskey' => '',
31            'target'    => 'target="_blank"',
32            'link'      => 'MetaModels',
33            'href'      => 'https://now.metamodel.me',
34        ];
35    }
36}

More on registering services in the linked article.