Add a Custom Link to the Magento 2 Top Menu

Magento’s top menu is compromised of categories chosen within the admin under the Catalog -> Categories menu. By default, this menu will only contain categories and nothing else, and therefore a common request from merchants is how to add a custom link to the Magento 2 top menu, usually a CMS page link.

Magento builds its top menu using the Magento\Catalog\Plugin\Block\Topmenu plugin via the beforeGetHtml() method.

// vendor/magento/module-catalog/Plugin/Block/Topmenu.php

<?php
namespace Magento\Catalog\Plugin\Block;

use Magento\Catalog\Model\Category;
use Magento\Framework\Data\Collection;
use Magento\Framework\Data\Tree\Node;

class Topmenu
{

    ....

    public function beforeGetHtml(
        \Magento\Theme\Block\Html\Topmenu $subject,
        $outermostClass = '',
        $childrenWrapClass = '',
        $limit = 0
    ) {
        $rootId = $this->storeManager->getStore()->getRootCategoryId();
        $storeId = $this->storeManager->getStore()->getId();
        /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */
        $collection = $this->getCategoryTree($storeId, $rootId);
        $currentCategory = $this->getCurrentCategory();
        $mapping = [$rootId => $subject->getMenu()];  // use nodes stack to avoid recursion
        foreach ($collection as $category) {
            $categoryParentId = $category->getParentId();
            if (!isset($mapping[$categoryParentId])) {
                $parentIds = $category->getParentIds();
                foreach ($parentIds as $parentId) {
                    if (isset($mapping[$parentId])) {
                        $categoryParentId = $parentId;
                    }
                }
            }

            /** @var Node $parentCategoryNode */
            $parentCategoryNode = $mapping[$categoryParentId];

            $categoryNode = new Node(
                $this->getCategoryAsArray(
                    $category,
                    $currentCategory,
                    $category->getParentId() == $categoryParentId
                ),
                'id',
                $parentCategoryNode->getTree(),
                $parentCategoryNode
            );
            $parentCategoryNode->addChild($categoryNode);

            $mapping[$category->getId()] = $categoryNode; //add node in stack
        }
    }

    ....

}

Similarly, a plugin can be created via a custom module that will be used to add a custom link.

Start by adding the module’s registration.php file.

// app/code/[Vendor]/[Module]/registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    '[Vendor]_[Module]',
    __DIR__
);

Then add the module.xml file.

// app/code/[Vendor]/[Module]/etc/module.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="[Vendor]_[Module]" setup_version="1.0.0"/>
</config>

When defining any plugin within Magento, the configuration is added within the di.xml. If you do not have this file within your module, you can create one and add in the configuration.

// app/code/[Vendor]/[Module]/etc/frontend/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Theme\Block\Html\Topmenu">
        <plugin name="[module]_topmenu" type="[Vendor]\[Module]\Plugin\AddLinkToTopmenu" />
    </type>
</config>

And finally, add in the Plugin class. As with the default Topmenu plugin, a beforeGetHtml() method is defined, and the menu node is added as part of the Magento\Framework\Data\Tree\NodeFactory class.

 // app/code/[Vendor]/[Module]/Plugin/AddLinkToTopmenu.php

<?php
namespace [Vendor]\[Module]\Plugin;

use Magento\Framework\Data\Tree\NodeFactory;

class AddLinkToTopmenu
{
    /**
     * @var NodeFactory
     */
    protected $nodeFactory;

    public function __construct(
        NodeFactory $nodeFactory
    ) {
        $this->nodeFactory = $nodeFactory;
    }

    /**
     * @param \Magento\Theme\Block\Html\Topmenu $subject
     * @param string $outermostClass
     * @param string $childrenWrapClass
     * @param int $limit
     */
    public function beforeGetHtml(
        \Magento\Theme\Block\Html\Topmenu $subject,
        $outermostClass = '',
        $childrenWrapClass = '',
        $limit = 0
    ) {
        $node = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray(),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $subject->getMenu()->addChild($node);
    }

    /**
     * @return array
     */
    protected function getNodeAsArray()
    {
        return [
            'name' => __('Custom Link'),
            'id' => 'custom-link',
            'url' => 'custom-page-url',
            'has_active' => false,
            'is_active' => false // (expression to determine if menu item is selected or not)
        ];
    }
}

After enabling the module, running Magento’s setup upgrade command and clearing the cache, the custom link will appear as part of the top menu.

Add a Custom Link to the Magento 2 Top Menu

As seen, the link is added to the end of the category menu items. With extra coding it probably is possible to add a link before the other items, however usually just having a custom menu item that isn’t a category is enough to please merchants.

Note: This article is based on Magento Open Source version 2.2.