How to add Products to Topmenu in Magento 2

Let’s say your company sells one product. This deserves the spotlight, right? Let’s say you want to give your product a prominent place in the topmenu. Since Magento 2 natively only supports adding categories to its topmenu, you’re up for a challenge! Today I’ll show you how to create a module which allows you to add products to your topmenu.

Creating a Magento 2 module to add Products to Topmenu

Like with everything in life, creating a module to add products to your topmenu is easy if you know what you’re doing. (Uh-duh…) Aren’t I great at SEO writing? To follow this tutorial you’ll need two cups of programming skills and a tea spoon of Magento (2) development skills.

For those of my loyal readers who came here hoping to find a ready-to-install module… GET OUT!

No, just kidding!

Scroll to the bottom of the page and enjoy your new fancy topmenu!

Now we separated the men/women from the boys/girls, let’s get down to business.

There are multiple ways to approach this. E.g. the Magento_Catalog module adds its categories using a Before-plugin to hook into Magento_Theme‘s getHtml()-method.

In this tutorial I will hook into the page_block_html_topmenu_gethtml_before-event using an Observer.

Why? Granted that plugins offer more freedom and flexibility and might even perform better, I feel events are a way of communicating among developers. Like one developer is whispering in another developer’s ear: ‘I’d like you to extend my code here…

So when an event is available, I’ll respect the effort to communicate and build an Observer instead of a Plugin. But I might update this post some time and add a plugin. Who knows!

Dexter whispering

Dexter (the awesomest developer in history) must’ve been whispering: “I’d like you to extend my code here…” Nothing French about cheese and eggs!

Building the Foundation

Before we start creating the Observer (which is basically all we need to do) we need to create a basic Magento 2-module. Consisting of two files:

  1. a registration.php,
  2. and an etc/module.xml.

In this example, the module is called Dan0sz/TopmenuProducts. Which means you should create that path inside your app/code-folder (case-sensitive) and add the following files:

registration.php

<?php
/**
* @author: Daan van den Bergh
* @url: https://daan.dev
* @package: Dan0sz/TopmenuProducts
* @copyright: (c) 2019 Daan van den Bergh
*/
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Dan0sz_TopmenuProducts',
__DIR__
);
view raw registration.php hosted with ❤ by GitHub

etc/module.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* @author: Daan van den Bergh
* @url: https://daan.dev
* @package: Dan0sz/TopmenuProducts
* @copyright: (c) 2019 Daan van den Bergh
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Dan0sz_TopmenuProducts" setup_version="1.0.0">
<sequence>
<module name="Magento_Catalog" />
</sequence>
</module>
</config>
view raw etc-module.xml hosted with ❤ by GitHub

Observing the Event

Before creating the Observer, we need to register our class to the event. In this case, the page_block_html_topmenu_gethtml_before-event.

etc/events.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
/**
* @author: Daan van den Bergh
* @url: https://daan.dev
* @package: Dan0sz/TopmenuProducts
* @copyright: (c) 2019 Daan van den Bergh
*/
-->
<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="dan0sz_topmenu_products_gethtml_before" instance="Dan0sz\TopmenuProducts\Observer\TopmenuGetHtmlBefore" />
</event>
</config>
view raw etc-events.xml hosted with ❤ by GitHub

Creating the Observer

This is the hardest part. Just remember. If you want to give up, that’s okay. No one is judging you. Except for Judgetin Timberlake over here:

Justin Timberlake looking judgy.

Judgy Justin

The file below requires some modification from your side. Referring to line 80, read the example code and comments I’ve added.

Observer/TopmenuGetHtmlBefore.php

<?php
/**
* @author : Daan van den Bergh
* @url : https://daan.dev
* @package : Dan0sz/TopmenuProducts
* @copyright: (c) 2019 Daan van den Bergh
*/
namespace Dan0sz\TopmenuProducts\Observer;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
use Magento\Framework\App\Request\Http as Request;
use Magento\Framework\Data\Tree\Node;
use Magento\Framework\Data\Tree\NodeFactory;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
class TopmenuGetHtmlBefore implements ObserverInterface
{
/** @var ProductCollection $productCollection */
private $productCollection;
/** @var Node $node */
private $node;
/** @var Request $request */
private $request;
/**
* TopmenuGetHtmlBefore constructor.
*
* @param ProductCollectionFactory $productCollection
* @param NodeFactory $node
*/
public function __construct(
ProductCollectionFactory $productCollection,
NodeFactory $node
) {
$this->productCollection = $productCollection;
$this->node = $node;
}
/**
* @param Observer $observer
*
* @return $this|void
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function execute(Observer $observer)
{
/** @var Node $menu */
$menu = $observer->getData('menu');
/** @var Request request */
$this->request = $observer->getData('request');
$products = $this->getTopmenuProducts();
$this->addProductsToMenu($products, $menu);
return $this;
}
/**
* @return ProductCollection
*/
private function getTopmenuProducts()
{
/** @var ProductCollection $productCollection */
$productCollection = $this->productCollection->create();
$productCollection->addAttributeToSelect(
[
'status',
'entity_id',
'name',
'url_key'
]
);
// This is where you add the entity_id's of the products you want to add to your menu.
$productCollection
->addAttributeToFilter('entity_id', ['in' => [1, 2, 3, 4]]);
// And perhaps another attribute by which you want to sort?
$productCollection->addAttributeToSort('entity_id');
return $productCollection;
}
/**
* @param ProductCollection $products
* @param Node $menu
*
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
private function addProductsToMenu(ProductCollection $products, Node $menu)
{
foreach ($products as $product) {
$node = $this->node->create(
[
'data' => $this->getProductAsArray($product),
'idField' => 'id',
'tree' => $menu->getTree()
]
);
$menu->addChild($node);
}
}
/**
* @param Product $product
*
* @return array
*/
private function getProductAsArray(Product $product)
{
return [
'name' => $product->getName(),
'id' => 'product-node-' . $product->getId(),
'url' => $product->getProductUrl(),
'has_active' => false,
'is_active' => $this->checkIsActive($product),
'is_category' => false,
'is_parent_active' => true
];
}
/**
* @param Product $product
*
* @return bool
*/
private function checkIsActive(Product $product)
{
return $product->getId() == $this->request->getParam('id');
}
}

The simplest way to add products to your topmenu is to specify their entity ID’s in an array, the downside to this is that you’d have to hard-code it. Making it a not-so transportable module between different stores.

Download Topmenu Products for Magento 2

In Top Menu Products for Magento 2 I added a product attribute, collecting all products having the ‘Add to Top Menu?’-attribute enabled. I added a sort order, to specify the order of the menu-items and a configurable option to display products before or after the existing category topmenu items.

I know, I can be a bit of a show-off!

Download it from Github or install it using composer by running composer require dan0sz/topmenu-products-magento2 from a terminal.

Summary

Add products to topmenu in Magento 2 can have many use-cases. In this tutorial I’ve shown you how to add products to Magento 2’s topmenu using an Observer. We’ve touched upon the discussion when to choose Plugins or Observers, but went along to use an Observer and implemented it into a basic Magento 2 plugin.