Containers and Blocks
A container is a named slot that holds things; a block is a class plus a template that renders HTML.
<referenceContainer name="content"><block class="..." name="..." template="..." after="..."/></referenceContainer>Two node types carry the page. A container is a named slot. It renders nothing of its own beyond an optional wrapping tag, and its job is to hold children: content, content.aside, sidebar.main, page.top, footer. A block is a PHP class with a template; it produces the HTML.
Adding to what already exists
<referenceContainer name="x"> and <referenceBlock name="x"> create nothing. They point at an element declared elsewhere, usually in a core module, and everything nested inside them is added to it. This is how a module changes a page it does not own.
Ordering
before and after name a sibling inside the same parent. The literal - is special: before="-" means first, after="-" means last. Neither reference node accepts before or after itself, so position the child, or change the parent with <move>.
Shorthand and longhand
<block class="..." name="acme.help" template="Acme_Delivery::help.phtml"/>
<block class="..." name="acme.help">
<arguments>
<argument name="template" xsi:type="string">Acme_Delivery::help.phtml</argument>
</arguments>
</block>
Those two are the same thing. Both forms appear in core code.
What trips people up
A container only exists if the active page layout declared it: sidebar.main is absent from a one-column page, and a block added to a missing container never renders. Run bin/magento cache:flush after the edit.
Example
<!-- app/code/Acme/Delivery/view/frontend/layout/catalog_category_view.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="sidebar.main">
<container name="acme.help" label="Category help"
htmlTag="div" htmlClass="acme-help" before="-">
<!-- template as an attribute -->
<block class="Magento\Framework\View\Element\Template"
name="acme.help.returns"
template="Acme_Delivery::category/returns.phtml"/>
<!-- the same thing, written as an argument -->
<block class="Magento\Framework\View\Element\Template" name="acme.help.contact">
<arguments>
<argument name="template" xsi:type="string">Acme_Delivery::category/contact.phtml</argument>
</arguments>
</block>
</container>
</referenceContainer>
</body>
</page>When to use it
- Put a returns-policy block at the top of the category sidebar.
- Wrap two blocks in a div with a class so the stylesheet can target them.
- Add a tab to the product details section without editing the core template.
- Reuse one block class with two templates on two pages.
More examples
A wrapper container that only exists in configuration
htmlTag plus htmlClass makes the container emit <div class="acme-badges"> around its children, which saves writing a wrapper template. htmlTag is required if you set htmlClass or htmlId. The ifconfig attribute hides the second block unless the configuration path acme_delivery/badges/warranty is on, so a merchant can switch it off without a deployment.
<!-- app/code/Acme/Delivery/view/frontend/layout/catalog_product_view.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">
<container name="acme.badges" htmlTag="div" htmlClass="acme-badges"
after="product.price.tier">
<block class="Magento\Framework\View\Element\Template"
name="acme.badge.returns"
template="Acme_Delivery::product/badge-returns.phtml"/>
<block class="Magento\Framework\View\Element\Template"
name="acme.badge.warranty"
template="Acme_Delivery::product/badge-warranty.phtml"
ifconfig="acme_delivery/badges/warranty"/>
</container>
</referenceContainer>
</body>
</page>Naming children for a parent template
name is unique across the page; as is the alias the parent uses to fetch the child, which is why core templates can call getChildHtml('acme_delivery'). The group attribute puts the block into the detailed_info group that the product details block renders as tabs, the same mechanism product.info.description and product.attributes use.
<!-- app/code/Acme/Delivery/view/frontend/layout/catalog_product_view.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="product.info.details">
<block class="Magento\Framework\View\Element\Template"
name="acme.delivery.tab"
as="acme_delivery"
group="detailed_info"
template="Acme_Delivery::product/tab.phtml">
<arguments>
<argument name="title" translate="true" xsi:type="string">Delivery</argument>
</arguments>
</block>
</referenceBlock>
</body>
</page>The containers you can target on a two-column page
The core file, unmodified. It shows why sidebar.main is missing on one-column pages: the name is created here, in the page layout, not in any page configuration file. When a block refuses to appear, open the page layout for the wireframe the page uses and check the container name exists.
<!-- vendor/magento/module-theme/view/frontend/page_layout/2columns-left.xml -->
<layout xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_layout.xsd">
<update handle="1column"/>
<referenceContainer name="columns">
<container name="div.sidebar.main" htmlTag="div" htmlClass="sidebar sidebar-main" after="main">
<container name="sidebar.main" as="sidebar_main" label="Sidebar Main"/>
</container>
<container name="div.sidebar.additional" htmlTag="div" htmlClass="sidebar sidebar-additional"
after="div.sidebar.main">
<container name="sidebar.additional" as="sidebar_additional" label="Sidebar Additional"/>
</container>
</referenceContainer>
</layout>
Discussion