Disclaimer:

This post assumes the reader is familiar with Magento 2 modules and how they work. If not, it is highly recommended to read the documentation first. All files mentioned below must be placed inside a module.

TL;DR

In order to add a custom layout handle to category page, a (basic) Magento 2 module with these additional files and their content is needed:

1. The events.xml file to “subscribe” to the event and say which observer should be fired
2. An observer that adds a new layout handle to the page
3. A layout file that adds needed changes to the page

The long story

First of all, why would you need to add a custom layout to some category? Well, you might need to achieve one of the following:

  • adjust some CSS specifically for that type of page
  • insert additional page information (like blocks and containers via layout)
  • add a custom JS to the page, like different tracking scripts and tracking pixels

To add custom layout to the category page we’ll use events and observers – it’s just one of the ways to extend the Magento 2 functionality.

Second, and the best choice would be Magento 2 plugins (interceptors) – but in our case, we can’t use it to add a custom layout to a category page. We are able to make a plugin for the Magento\Catalog\Controller\Category\View.php and use one of afterExecute or aroundExecute methods to attach the custom layout handle to the page. But, unfortunately, its (custom layout handle) changes won’t be applied to the generated page.

Turning back to observers and events, there is a great article on how to understand which event fits you best. For our purpose, we’ll use the layout_load_before because there is no other point (for categories) to append custom layout until the page is rendered.

We’ll subscribe for layout_load_before event in the events file (events.xml), which in Magento 2 is placed under the etc directory of a module. Actually, there are 3 places where to keep the events file:

  1. /etc directory – /etc/events.xml. That means events will trigger on both storefront and admin side.
  2. /etc/frontend directory – /etc/frontend/events.xml. That means all the events will trigger on storefront only.
  3. /etc/adminhtml directory – /etc/adminhtml/events.xml. That means all the events will trigger on admin side only.

We’re going to change the category storefront layout, so we’ll place our changes in /etc/frontend/events.xml file – this makes the module changes more specific, avoid running multiple times when not needed. So far the file looks like this:

<?xml version="1.0" encoding="UTF-8" ?>
<!-- file: [Vendor]/[ModuleName]/etc/frontend/events.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="layout_load_before">
    </event>
</config>

Well, that’s not enough for our goal.

To catch the event, observers are used. According to Magento 2 Events and observers documentation, an observer element inside a events.xml file may have the following properties:

  • name (required) – The name of the observer for the event definition.
  • instance (required) – The fully qualified class name of the observer.
  • disabled – Determines whether this observer is active or not. Default value is false.
  • shared – Determines the lifestyle of the class. Default is false.

To check what properties are available for observer XML element, we may also check the vendor/magento/framework/Event/etc/events.xsd:48 file (the events.xml file config element suggests the path), where it says the same in technical mode:

<!-- file: vendor/magento/framework/Event/etc/events.xsd -->

<xs:complexType name="observerDeclaration">
    <xs:annotation>
        <xs:documentation>
            Observer declaration.
        </xs:documentation>
    </xs:annotation>
    <xs:attribute name="name" type="xs:string" use="required" />
    <xs:attribute name="instance" type="xs:string" use="optional" />
    <xs:attribute name="disabled" type="xs:boolean" use="optional" />
    <xs:attribute name="shared" type="xs:boolean" use="optional" />
</xs:complexType>

For every event there may be a lot of observers. The main rule here is: Observer names must be unique per event definition” – Magento 2 Documentation.

Keeping all this in mind, here is our final events.xml file:

<?xml version="1.0" encoding="UTF-8" ?>
<!-- file: [Vendor]/[ModuleName]/etc/frontend/events.xml -->

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="layout_load_before">
        <observer name="[vendor][modulename]_addcustomcategoryhandle"
                  instance="[Vendor]\[ModuleName]\Observer\AddCategoryLayoutUpdateHandleObserver" />
    </event>
</config>

Enough with XML stuff, let’s move forward writing the observer itself.

Before writing the code, let’s write down what we need to do:

  1. Check current page layout is for category
  2. Check for category in Magento’s registry
  3. Grab the layout update from the event
  4. Add the custom handle based on category attribute (we’ll check for the Display Mode)

Sounds like a plan. Here is how the observer looks like:

<?php
// file: [Vendor]/[ModuleName]/Observer/AddCategoryLayoutUpdateHandleObserver.php
declare(strict_types=1);

namespace [Vendor]\[ModuleName]\Observer;

use Magento\Catalog\Model\Category as CategoryModel;
use Magento\Framework\Event;
use Magento\Framework\Event\Observer as EventObserver;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Registry;
use Magento\Framework\View\Layout as Layout;
use Magento\Framework\View\Layout\ProcessorInterface as LayoutProcessor;

/**
 *  AddCategoryLayoutUpdateHandleObserver
 */
class AddCategoryLayoutUpdateHandleObserver implements ObserverInterface
{
    /**
     * Category Custom Layout Name
     *
     * It's the filename of layout phisically located
     * at `[Vendor]/[ModuleName]/view/frontend/layout/catalog_category_view_custom_layout.xml`
     */
    const LAYOUT_HANDLE_NAME = 'category_custom_layout_observer';

    /**
     * @var Registry
     */
    private $registry;

    /**
     * @param Registry $registry
     */
    public function __construct(Registry $registry)
    {
        $this->registry = $registry;
    }

    /**
     * @param EventObserver $observer
     *
     * @return void
     */
    public function execute(EventObserver $observer)
    {
        /** @var Event $event */
        $event = $observer->getEvent();
        $actionName = $event->getData('full_action_name');
        /** @var CategoryModel|null $category **/
        $category = $this->registry->registry('current_category');

        if (
            $category &&
            $actionName === 'catalog_category_view'
        ) {
            /** @var Layout $layout */
            $layout = $event->getData('layout');

            /** @var LayoutProcessor $layoutUpdate */
            $layoutUpdate = $layout->getUpdate();

            // check if Category Display Mode is "Mixed"
            if ($category->getData('display_mode') === CategoryModel::DM_MIXED) {
                $layoutUpdate->addHandle(static::LAYOUT_HANDLE_NAME);
            }
        }
    }
}

Writing the observer only doesn’t give us the desired result. We need to create the layout file that will do needed changes. In this case – adding a new class to the body.

The custom layout file, we named it catalog_category_view_custom_layout.xml (in AddCategoryLayoutUpdateHandleObserver.php:25) has to be placed under [Vendor]/[ModuleName]/view/frontend/layout directory and look similar to this:

<?xml version="1.0"?>
<!-- file: [Vendor]/[ModuleName]/view/frontend/layout/catalog_category_view_custom_layout.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <attribute name="class" value="category-custom-layout" />
        <!-- here goes additional layout manipulation -->
    </body>
</page>

With that, a new layout handle should be added to the category view page. That can be checked by having a look on the DOM element body – if our new class is added, then the layout was applied.

Congratulations!