33

I'm not sure what I'm doing wrong here. The block which holds the category links in is referenced to as navigation.sections. I thought by directing the following arguments toward the container I would be able to create a new link under it. Any help is appreciated.

<referenceContainer name="navigation.sections">
            <block class="Magento\Framework\View\Element\Html\Links" name="mylink">
                    <arguments>
                        <argument name="label" xsi:type="string">Mylink</argument>
                        <argument name="path" xsi:type="string">mypath</argument>
                        <argument name="css_class" xsi:type="string">mycss</argument>
                    </arguments>
            </block>
</referenceContainer>
3
  • I'm wondering the same.. Have you found a solution for it?
    – user34741
    Jan 5, 2016 at 19:50
  • Both solutions listed have worked for me. Jan 8, 2016 at 4:41
  • What Magento version are you working on? Oct 26, 2017 at 9:13

12 Answers 12

39

[EDIT]
Apparently, in latest versions of M2 this does not work anymore.
Thanks to Max for pointing this out.
For later version you need to add a plugin for Magento\Theme\Block\Html\Topmenu instead of an observer.
Add this to etc/frontend/di.xml

<type name="Magento\Theme\Block\Html\Topmenu">
    <plugin name="[module]-topmenu" type="[Namespace]\[Module]\Plugin\Block\Topmenu" />
</type>

and create the plugin class file [Namespace]/[Module]/Plugin/Block/Topmenu.php

<?php 

namespace [Namespace]\[Module]\Plugin\Block;

use Magento\Framework\Data\Tree\NodeFactory;

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

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

    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);
    }

    protected function getNodeAsArray()
    {
        return [
            'name' => __('Label goes here'),
            'id' => 'some-unique-id-here',
            'url' => 'http://www.example.com/',
            'has_active' => false,
            'is_active' => false // (expression to determine if menu item is selected or not)
        ];
    }
}

[/EDIT]
Original answer:
You can add elements to the top menu using the event page_block_html_topmenu_gethtml_before.

So you need to create a module with these files (all the files should be in app/code/[Namespace]/[Module]):

etc/module.xml - the module declaration file

<?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="[Namespace]_[Module]" setup_version="2.0.0">
        <sequence>
            <module name="Magento_Theme"/>
        </sequence>
    </module>
</config>

registration.php - the registration file

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

etc/frontend/events.xml - the events declaration file

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="page_block_html_topmenu_gethtml_before">
        <observer name="[namespace]_[module]_observer" instance="[Namespace]\[Module]\Observer\Topmenu" />
    </event>
</config>

Observer/Topmenu.php - the actual observer

<?php
namespace [Namespace]\[Module]\Observer;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Data\Tree\Node;
use Magento\Framework\Event\ObserverInterface;
class Topmenu implements ObserverInterface
{
    public function __construct(
        ...//add dependencies here if needed
    )
    {
    ...
    }
    /**
     * @param EventObserver $observer
     * @return $this
     */
    public function execute(EventObserver $observer)
    {
        /** @var \Magento\Framework\Data\Tree\Node $menu */
        $menu = $observer->getMenu();
        $tree = $menu->getTree();
        $data = [
            'name'      => __('Menu item label here'),
            'id'        => 'some-unique-id-here',
            'url'       => 'url goes here',
            'is_active' => (expression to determine if menu item is selected or not)
        ];
        $node = new Node($data, 'id', $tree, $menu);
        $menu->addChild($node);
        return $this;
    }
}

Now run in the cli php bin/magento setup:upgrade to install the module and you are good to go.

31
  • Is Topmenu.php missing part of the code? Jan 7, 2016 at 16:42
  • 1
    @Solide. The order of the links depend on the order the observers are executed. If your homepage observer is executed before the catalog one then the homepage link should be added first. If not, you can take a look at this approach for changing the order of the links: magento.stackexchange.com/q/7329/146. the approach is for Magento1, but you can translate it to M2 code.
    – Marius
    Feb 25, 2016 at 13:29
  • 1
    @Marius : what should be the 'is_active'. Please add some example. i want active link on this page. Mar 30, 2016 at 10:37
  • 1
    An observer is used on an event. A plugin can work on any public method. I would advice to use the plugin approach since that one is used in the core to add the categories to the top menu.
    – Marius
    Aug 1, 2017 at 14:38
  • 1
    Sorry, I feel like an idiot, but how can you add more than one menu? If I use $menu->addChild($node) more than once, the last one override the other ones. It only shows one menu (the last one).
    – pinicio
    Feb 23, 2018 at 15:16
20

Why does everybody always want to write a module. I did this in my layout.xml and it worked like a charm:

    <referenceBlock name="catalog.topnav">
        <block class="Magento\Framework\View\Element\Html\Link" name="contact-link">
            <arguments>
                <argument name="label" xsi:type="string" translate="true">Contact us</argument>
                <argument name="path" xsi:type="string" translate="true">contact</argument>
            </arguments>
        </block>
    </referenceBlock>
6
  • how to open that link in new tab? Sep 28, 2018 at 9:33
  • Good question. Found something in the code. Maybe try this: <argument name="attributes" xsi:type="array"> <item name="target" xsi:type="string">_blank</item> </argument> Not tested, but there is the attributes option available. Sep 28, 2018 at 13:09
  • 1
    Creating a module makes it much more dynamic. A lot of customers i work with want to do things themselves like in this case creating pages and add them to the topmenu in a specific order.
    – Prometheus
    Mar 22, 2019 at 14:14
  • @jafarpinjar this will do the work: <argument name="target" xsi:type="string">_blank</argument> Jan 14, 2020 at 8:24
  • @YogeshAgarwal, Thank you, let me try it Jan 14, 2020 at 9:29
8

This answer is provide by Marius♦ i have just modified it to add child category in category tab menu you can refer Marius♦'s answer. I just modified child Topmenu.php file to add child category in main category

<?php 

namespace Ktpl\Navigationlink\Plugin\Block;

use Magento\Framework\UrlInterface;
use Magento\Framework\Data\Tree\NodeFactory;
use Magento\Store\Model\StoreManagerInterface;

class Topmenu
{
    /**
     * @var NodeFactory
     */
    protected $nodeFactory;
    protected $urlBuilder;
    protected $_storeManager;

    public function __construct(
        UrlInterface $urlBuilder,
        NodeFactory $nodeFactory,
        StoreManagerInterface $storeManager
    ) {
        $this->urlBuilder = $urlBuilder;
        $this->nodeFactory = $nodeFactory;
        $this->_storeManager = $storeManager;
    }

    public function beforeGetHtml(
        \Magento\Theme\Block\Html\Topmenu $subject,
        $outermostClass = '',
        $childrenWrapClass = '',
        $limit = 0
    ) {
        // condition for store
        if($this->getStoreCode() == 'store_id'):
        $productNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Products','products'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $stockistsNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Stockists','stockists'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $ourstoryNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Our Story','ourstory'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $contactsNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Customer Care','contacts'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        /******* contacts's child *******/
        $warrantyRegistrationNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Warranty Registration','warranty-registration'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $faqNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Frequently Asked Questions','faq'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $ourProductGuaranteeNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Our Product Guarantee','our-product-guarantee'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $warrantiesNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Warranties, Repairs & Spare Parts','warranties-repairs-spare-parts'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $termsNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Terms & Conditions','terms-and-conditions'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $privacyPolicyNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Our Privacy Policy','privacy-policy'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );
        $bookNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArray('Book A Viewing','book-a-viewing'),
                'idField' => 'id',
                'tree' => $subject->getMenu()->getTree()
            ]
        );

        $contactsNode->addChild($warrantyRegistrationNode);
        $contactsNode->addChild($faqNode);
        $contactsNode->addChild($ourProductGuaranteeNode);
        $contactsNode->addChild($warrantiesNode);
        $contactsNode->addChild($termsNode);
        $contactsNode->addChild($privacyPolicyNode);
        $contactsNode->addChild($bookNode);
        /******* end contacts's child *******/

        $subject->getMenu()->addChild($productNode);
        $subject->getMenu()->addChild($stockistsNode);
        $subject->getMenu()->addChild($ourstoryNode);
        $subject->getMenu()->addChild($contactsNode);
        endif;
    }

    protected function getNodeAsArray($name,$id)
    {
        return [
            'name' => __($name),
            'id' => $id,
            'url' => $this->urlBuilder->getUrl($id),
            'has_active' => false,
            'is_active' => false // (expression to determine if menu item is selected or not)
        ];
    }

    public function getStoreCode()
    {
        return $this->_storeManager->getStore()->getCode();
    }
}

You need to create node for parent category and for child category and after that you can assign child category to parent category by using addChild method here is an example

$contactsNode->addChild($warrantyRegistrationNode);
2
  • Thanks! didn't realise it's was that easy to add Submenu! Dec 12, 2018 at 12:47
  • and Sir if I want to show my custom div on the custom Link I added in Topmenu. Like when I mouse hover on the Link then it shows my custom div
    – Asad Khan
    Apr 30, 2019 at 15:51
7

Another solution outside of creating a module is overwriting topmenu.phtml. I will note that the solution provided by @Marius is the best way to do this if you intend for your links to inherit the navigation classes. This does show in Magento's mobile menu, just without the proper css. You could use the css_class argument to style accordingly.

YourTheme/Magento_Theme/templates/html/topmenu.phtml

<?php $columnsLimit = $block->getColumnsLimit() ?: 0; ?>
<?php $_menu = $block->getHtml('level-top', 'submenu', $columnsLimit) ?>

<nav class="navigation" role="navigation">
    <ul data-mage-init='{"menu":{"responsive":true, "expanded":true, "position":{"my":"left top","at":"left bottom"}}}'>
        <?php /* @escapeNotVerified */ echo $_menu; ?>
        <?php echo $block->getChildHtml() ?>
    </ul>
</nav>

YourTheme/Magento_Theme/layout/default.xml

<referenceContainer name="catalog.topnav">
               <block class="Magento\Framework\View\Element\Html\Link\Current" name="your.link">
                    <arguments>
                        <argument name="label" xsi:type="string">Link-name</argument>
                        <argument name="path" xsi:type="string">Link-url</argument>
                    </arguments>
              </block>
</referenceContainer>
3
3

Want to add a link to the top navigation within <header>
Adding a link to CMS page, Gallery

Edit/Place default.xml here:

app/design/frontend/Vendor/theme/Magento_Theme/layout/default.xml

Add the following code:

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="catalog.topnav">
           <block class="Magento\Framework\View\Element\Html\Link\Current" name="gallery.link">
                <arguments>
                    <argument name="label" xsi:type="string">Gallery</argument>
                    <argument name="path" xsi:type="string">gallery</argument>
                </arguments>
          </block> 
       </referenceContainer>
    </body>
</page>

This adds a link to CMS page, Gallery, with the following settings:

Title = Gallery
Url Key = gallery
Link = https://example.com/gallery/

Add the following styling to ensure the new link aligns correctly:

.navigation .nav.item {
margin: 0 10px 0 0;
display: inline-block;
position: relative;
}

Results of the code (Products is setup as a category for an example)

3

You can get this by plugin after method

app/code/Vendor/Module/etc/di.xml

<type name="Magento\Theme\Block\Html\Topmenu">
    <plugin name="add_category_custom_item" type="Vendor\Module\Plugin\CategoryCustomItemPlugin" />
</type>

Now create app/code/Vendor/Module/Plugin/CategoryCustomItemPlugin.php

<?php

namespace Vendor\Module\Plugin;

class CategoryCustomItemPlugin
{

    public function afterGetHtml(\Magento\Theme\Block\Html\Topmenu $topmenu, $html)
    {
        $customurl= $topmenu->getUrl('yoururl'); //here you can set link
        $CurrentUrl = $topmenu->getUrl('*/*/*', ['_current' => true, '_use_rewrite' => true]);
        if (strpos($CurrentUrl,'yoururl') !== false) {
            $html .= "<li class=\"level0 nav-9 category-item level-top ui-menu-item active\">";
        } else {
            $html .= "<li class=\"level0 nav-9 category-item level-top ui-menu-item\">";
        }
        $html .= "<a href=\"" . $customurl. "\" class=\"level-top ui-corner-all\"><span class=\"ui-menu-icon ui-icon ui-icon-carat-1-e\"></span><span>" . __("yoururl") . "</span></a>";
        $html .= "</li>";
        return $html;
    }
}
1
  • Yes, Thank You.
    – Bhavesh
    Jun 20, 2020 at 12:15
2

Using the above answer by Marius I added submenu items. I also show a way you can edit the tree before the html is created and then how to edit the html directly once it is created. It works in Magento 2.1 (EDIT: still working in 2.2.11). Update Topmenu.php with this:

<?php
namespace Seatup\Navigation\Observer;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Data\Tree\Node;
use Magento\Framework\Event\ObserverInterface;
class Topmenu implements ObserverInterface
{
    protected $_cmsBlock;

    public function __construct(
        \Magento\Cms\Block\Block $cmsBlock
    )
    {
        $this->_cmsBlock = $cmsBlock;
    }
    /**
     * @param EventObserver $observer
     * @return $this
     */
    public function execute(EventObserver $observer)
    {
        /** @var \Magento\Framework\Data\Tree\Node $menu */
        $eventName = $observer->getEvent()->getName();
        if($eventName == 'page_block_html_topmenu_gethtml_before'){
            // With the event name you can edit the tree here
            $menu = $observer->getMenu();
            $tree = $menu->getTree();
            $children = $menu->getChildren();

            foreach ($children as $child) {
                if($child->getChildren()->count() > 0){ //Only add menu items if it already has a dropdown (this could be removed)
                    $childTree = $child->getTree();
                    $data1 = [
                        'name'      => __('Menu item label here'),
                        'id'        => 'some-unique-id-here',
                        'url'       => 'url goes here',
                        'is_active' => FALSE
                    ];
                    $node1 = new Node($data1, 'id', $childTree, $child);
                    $childTree->addNode($node1, $child);
                }
            }
            return $this;
        } else if($eventName == 'page_block_html_topmenu_gethtml_after'){
            // With the event name you can edit the HTML output here
            $transport = $observer['transportObject'];

            //get the HTML
            $old_html = $transport->getHtml();

            //render the block. I am using a CMS block
            $new_output = $this->_cmsBlock->getLayout()->createBlock('Magento\Cms\Block\Block')->setBlockId('cms_block_identifier')->toHtml();
            //the transport now contains html for the group/class block
            //which doesn't matter, because we already extracted the HTML into a 
            //string primitive variable
            $new_html = str_replace('to find', $new_output , $old_html);    
            $transport->setHtml($new_html);
        }
    }
}
1

If you want to add CMS Pages or other this would be best

https://github.com/Mestrona/Mestrona_CategoryRedirect

Worked for me :)

1
  • I tried this module but it not work in Magento 2.1.3 Dec 29, 2016 at 9:47
1

Just for a navigation menu link, there is no much steps to achieve, I've found a short tutorial on doing that, it implies a theme which overrides the topmenu.phtml file from the Magento_Theme module : https://linkstraffic.net/adding-custom-menu-item-inside-magento2/ I've tested it successfully, so I share it with you guys.

1
  • Welcome to Magento SE. If you post links in an answer, please make sure that the answer is still valuable, if the link becomes dead at some time: For example, summarize the linked article or quote the relevant parts. This is important because StackExchange aims to be a knowledge database, not a support forum that helps one person right now. Future visitors should still benefit from the questions and answers. Jun 29, 2017 at 22:29
1

For those looking to add is_active expression, especially @zed Blackbeard who asked above.

I've used to link the contact and it will work with custom module too as I am linking to one.

'is_active' => ($this->request->getFrontName() == 'contact' ? true : false)

// (expression to determine if menu item is selected or not)

Hope it helps anyone.

1

This is also a good option:

app/design/frontend/Vender/yourtheme/Magento_Theme/layout/default.xml

<referenceBlock name="header.links">
    <block class="Magento\Framework\View\Element\Html\Link" name="yourlinkname" before='wish-list-link'>
        <arguments>
            <argument name="label" xsi:type="string" translate="true">yourlink</argument>
            <argument name="path" xsi:type="string" translate="true">yourlink</argument>
        </arguments>
    </block>
</referenceBlock>
0

Use below code to add menu and sub-menu / Child menu

Vendor/Module/etc/frontend/events.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="page_block_html_topmenu_gethtml_before">
        <observer name="custom_productmenu_observer" instance="Vendor\Module\Observer\Topmenu" />
    </event>
</config>

Vendor/Module/Observer/Topmenu.php

<?php
namespace Vendor\Module\Observer;
use Magento\Framework\Data\Tree\Node;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;

class Topmenu implements ObserverInterface {
    protected $nodeFactory;
    protected $urlBuilder;

    public function __construct(
        \Magento\Framework\Data\Tree\NodeFactory $nodeFactory,
        \Magento\Framework\UrlInterface $urlBuilder
    ) {
        $this->nodeFactory = $nodeFactory;
        $this->urlBuilder = $urlBuilder;
    }

    public function execute(EventObserver $observer) {
        $menu = $observer->getMenu();
        $menuNode = $this->nodeFactory->create(
            [
                'data' => $this->getNodeAsArrayzv("Menu Custom", "custom-menu", "/subtrate.html?sub=336"),
                'idField' => 'id',
                'tree' => $menu->getTree(),
            ]
        );
        $menuNode->addChild(
            $this->nodeFactory->create(
                [
                    'data' => $this->getNodeAsArrayzv("Custom Menu - Child Menu", "child-menu", "/subtrate.html?sub=336"),
                    'idField' => 'id',
                    'tree' => $menu->getTree(),
                ]
            )
        );

        $menuNode->addChild(
            $this->nodeFactory->create(
                [
                    'data' => $this->getNodeAsArrayzv("Custom Menu - Child Menu2", "child-menu1", "/subtrate.html?sub=356"),
                    'idField' => 'id1',
                    'tree' => $menu->getTree(),
                ]
            )
        );
        $menu->addChild($menuNode);
        return $this;
    }

    protected function getNodeAsArrayzv($name, $id, $url) {
        return [
            'name' => __($name),
            'id' => $id,
            'url' => $url,
            'has_active' => false,
            'is_active' => false,
        ];
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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