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).
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.