Handles
A handle names a set of layout instructions and the file that holds them; the handles that apply decide which files load.
<handle>.xml — e.g. default.xml, catalog_product_view.xml, catalog_product_view_type_configurable.xmlA handle is a name for a set of layout instructions. It is also a file name: a file called catalog_product_view.xml declares instructions for the handle catalog_product_view. Magento works out which handles apply to the request, then loads every file named after each of them from every module and from the theme.
The handles on a storefront page
default— applies to almost every page. Site-wide changes go indefault.xml.- The full action name: route, controller and action joined by underscores and lowercased. The product page is
catalog_product_view. - Page handles, which append one entity detail to the action name:
catalog_product_view_type_simple,catalog_product_view_id_42,catalog_product_view_sku_24-MB01,catalog_category_view_type_layered,catalog_category_view_displaymode_products. - Arbitrary handles, which apply to nothing by themselves and are pulled in with
<update handle="..."/>.default_head_blocksis one.
Where they come from
The controller's result page adds them. Magento\Catalog\Helper\Product\View calls addPageLayoutHandles(['type' => $product->getTypeId()]), and that call is what produces the _type_simple handle. An administrator can also attach a file-backed handle to one product, named catalog_product_view_selectable_<sku>_<name>.xml; since 2.3.4 the admin field is a picker over those files rather than free text.
What trips people up
The action-name part is lowercase with underscores and must match exactly. A file named catalog-product-view.xml is never loaded and never reports an error. After adding a layout file, run bin/magento cache:flush.
Example
<!-- app/code/Acme/Delivery/view/frontend/layout/catalog_product_view_type_configurable.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="product.info.main">
<block class="Magento\Framework\View\Element\Template"
name="acme.size.guide"
template="Acme_Delivery::product/size-guide.phtml"
after="product.info.price"/>
</referenceContainer>
</body>
</page>When to use it
- Show a size guide on configurable products only.
- Add a banner to every storefront page without touching a core file.
- Share one set of layout instructions between the cart page and a custom page.
- Work out the handle name for a controller action you just wrote.
More examples
A handle that applies everywhere
The core Contact module, verbatim. It is the pattern to copy for a site-wide footer link: default.xml, a referenceBlock on footer_links, and ifconfig so the link disappears when the feature is switched off in configuration. Copy it into your own module's default.xml, change the label and path, then run bin/magento cache:flush.
<!-- vendor/magento/module-contact/view/frontend/layout/default.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="footer_links">
<block class="Magento\Framework\View\Element\Html\Link\Current"
ifconfig="contact/contact/enabled" name="contact-us-link">
<arguments>
<argument name="label" xsi:type="string" translate="true">Contact Us</argument>
<argument name="path" xsi:type="string">contact</argument>
</arguments>
</block>
</referenceBlock>
</body>
</page>One set of instructions, two pages
acme_delivery_promise is an arbitrary handle: no request produces it, so the first file does nothing on its own. The cart page file includes it with <update>, and any other handle can include it the same way. Reach for this when two unrelated pages need identical instructions and you do not want the block declared twice.
<!-- app/code/Acme/Delivery/view/frontend/layout/acme_delivery_promise.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<block class="Magento\Framework\View\Element\Template"
name="acme.delivery.promise"
template="Acme_Delivery::promise.phtml"/>
</referenceContainer>
</body>
</page>
<!-- app/code/Acme/Delivery/view/frontend/layout/checkout_cart_index.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<update handle="acme_delivery_promise"/>
<body/>
</page>A handle tied to one entity
Magento\Catalog\Controller\Category\View calls addPageLayoutHandles(['id' => $category->getId()]), so this file loads only for category 3. It works, and it is usually a trap: the id differs between staging and production, so the file silently does nothing after a database refresh. Prefer the type or displaymode handles, or a configuration flag, unless the entity really is fixed.
<!-- app/code/Acme/Delivery/view/frontend/layout/catalog_category_view_id_3.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="page.main.title" display="false"/>
</body>
</page>
Discussion