Page Builder was introduced with Magento Commerce 2.3.1. It’s the successor of “Bluefoot“, an advanced, intuitive and very flexible CMS tool for individual layouts and content elements.
A typical task for a developer is to create a new content element with a few custom fields. In the end, it should output a given HTML snippet, including information from the custom fields. This little tutorial will show you how to create a simple content element.
How Page Builder works
The basic principles of Page Builder are different from those of Bluefoot or the default Magento CMS. Page Builder uses JavaScript and UI components heavily. It consists of the following parts:
- A backend form, defined by UI components (XML)
- A frontend output template, called master template (HTML)
- An XML configuration file including a mapping between input fields (in the backend form) and output fields (in the master template)
- A JavaScript engine which fills the master template with the correct field values and saves the generated HTML in the database
Example: Hero Teaser
We use a simplified example for showing the general principles. We want to display a so-called “hero teaser” – an image with a headline and a “call to action” link.
The Directory Structure
I recommend having one Magento module per Page Builder element. In this case, it’s named “PageBuilderHeroTeaser” and can either be in app/code/<namespace>/
or included via composer. While there is nothing special in the module base-files (“composer.json”, “registration.php”, “etc/module.xml”), you can see that all Page Builder specific files are in the “view” directory.
These files serve the following purposes:
- view/adminhtml/layout/pagebuilder_hero_teaser_form.xml: A few lines to include the backend form into the Page Builder. Nothing more than copy & paste.
- view/adminhtml/pagebuilder/content_type/hero_teaser.xml: The main configuration file for the element
- view/adminhtml/ui_component/pagebuilder_hero_teaser_form.xml: The backend form definition
- view/adminhtml/web/template/content-type/hero-teaser/default/master.html: The master template which is used for rendering of the frontend
- view/adminhtml/web/template/content-type/hero-teaser/default/preview.html: Similar to the master.html, but for the backend view (“Preview”)
The Page Builder reads all files in the “view/adminhtml/pagebuilder/content_type” directory of each module. In our example, the main configuration file “view/adminhtml/pagebuilder/content_type/hero_teaser.xml” contains the following line:
1 |
form="pagebuilder_hero_teaser_form" |
With this information, the Page Builder knows to load the “pagebuilder_hero_teaser_form.xml” file from the “view/adminhtml/layout” directory.
The Files
view/adminhtml/layout/pagebuilder_hero_teaser_form.xml
1 2 3 4 5 6 7 8 9 |
<?xml version="1.0"?> <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="admin-1column" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <update handle="styles"/> <body> <referenceContainer name="content"> <uiComponent name="pagebuilder_hero_teaser_form"/> </referenceContainer> </body> </page> |
This file includes the new backend form into the Page Builder areas. Please note that the name (here: “pagebuilder_hero_teaser_form”) is the name of the UI component file.
view/adminhtml/ui_component/pagebuilder_hero_teaser_form.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
<?xml version="1.0" encoding="UTF-8"?> <form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd" extends="pagebuilder_base_form_with_background_attributes"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="provider" xsi:type="string">pagebuilder_hero_teaser_form.pagebuilder_hero_teaser_form_data_source</item> </item> <item name="label" xsi:type="string" translate="true">Hero Teaser</item> </argument> <settings> <namespace>pagebuilder_hero_teaser_form</namespace> <deps> <dep>pagebuilder_hero_teaser_form.pagebuilder_hero_teaser_form_data_source</dep> </deps> </settings> <dataSource name="pagebuilder_hero_teaser_form_data_source"> <argument name="data" xsi:type="array"> <item name="js_config" xsi:type="array"> <item name="component" xsi:type="string">Magento_PageBuilder/js/form/provider</item> </item> </argument> <dataProvider name="pagebuilder_hero_teaser_form_data_source" class="Magento\PageBuilder\Model\ContentType\DataProvider"> <settings> <requestFieldName/> <primaryFieldName/> </settings> </dataProvider> </dataSource> <fieldset name="appearance_fieldset" sortOrder="10" component="Magento_PageBuilder/js/form/element/dependent-fieldset"> <settings> <label translate="true">Appearance</label> <additionalClasses> <class name="admin__fieldset-visual-select-large">true</class> </additionalClasses> <collapsible>false</collapsible> <opened>true</opened> <imports> <link name="hideFieldset">${$.name}.appearance:options</link> <link name="hideLabel">${$.name}.appearance:options</link> </imports> </settings> <field name="appearance" formElement="select" sortOrder="10" component="Magento_PageBuilder/js/form/element/dependent-visual-select"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="default" xsi:type="string">default</item> </item> </argument> <settings> <additionalClasses> <class name="admin__field-wide">true</class> <class name="admin__field-visual-select-container">true</class> </additionalClasses> <dataType>text</dataType> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> </validation> <elementTmpl>Magento_PageBuilder/form/element/visual-select</elementTmpl> </settings> <formElements> <select> <settings> <options class="AppearanceSourceHeroTeaser"/> </settings> </select> </formElements> </field> </fieldset> <fieldset name="general" sortOrder="20"> <settings> <label/> </settings> <field name="headline" sortOrder="10" formElement="input"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">page</item> </item> </argument> <settings> <dataScope>headline</dataScope> <dataType>text</dataType> <label translate="true">Headline</label> </settings> </field> <field name="link_url" sortOrder="60" formElement="input"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">page</item> </item> </argument> <settings> <dataScope>link_url</dataScope> <dataType>text</dataType> <label translate="false">Link URL</label> </settings> </field> <field name="link_text" sortOrder="70" formElement="input"> <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">page</item> </item> </argument> <settings> <dataScope>link_text</dataScope> <dataType>text</dataType> <label translate="false">Link Text</label> </settings> </field> </fieldset> </form> |
This file contains the backend form definition. Please note the marked lines especially, as they define the three fields we want to have: headline, link_url, and link_text. In this case, they are all text fields. For more field types please see the existing page elements delivered with the Page Builder module.
The form then looks like this:
view/adminhtml/pagebuilder/content_type/hero_teaser.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_PageBuilder:etc/content_type.xsd"> <type name="hero_teaser" label="Hero Teaser" menu_section="media" component="Magento_PageBuilder/js/content-type" preview_component="Magento_PageBuilder/js/content-type/preview" master_component="Magento_PageBuilder/js/content-type/master" form="pagebuilder_hero_teaser_form" icon="icon-pagebuilder-banner" sortOrder="31" translate="label"> <children default_policy="deny"/> <appearances> <appearance name="default" default="true" preview_template="Namespace_PageBuilderHeroTeaser/content-type/hero-teaser/default/preview" master_template="Namespace_PageBuilderHeroTeaser/content-type/hero-teaser/default/master" reader="Magento_PageBuilder/js/master-format/read/configurable"> <elements> <element name="main"> <attribute name="name" source="data-content-type"/> <attribute name="appearance" source="data-appearance"/> <css name="css_classes"/> </element> <element name="headline"> <html name="headline" converter="Magento_PageBuilder/js/converter/html/tag-escaper"/> </element> <element name="link"> <attribute name="link_url" source="href"/> <html name="link_text" converter="Magento_PageBuilder/js/converter/html/tag-escaper"/> </element> </elements> </appearance> </appearances> </type> </config> |
This file contains the general page element configuration. At the top, you see the identifier (“name”), label, icon and sort order as well as the components used for rendering frontend (“master”) and backend (“preview”) output. We use the default components here: “Magento_PageBuilder/js/content-type/master” and “Magento_PageBuilder/js/content-type/preview”.
In the appearance section (you can have several appearances which are different layouts for the element), you find links to the preview_template and master_template. In the “elements” definition below, you can see a mapping between the admin form fields (names “attributes”) and the template fields (“elements”).
view/adminhtml/web/template/content-type/hero-teaser/default/master.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<div attr="data.main.attributes" ko-style="data.main.style" class="hero-teaser" css="data.main.css"> <div> <section class="hero-teaser__content"> <div class="container"> <div class="hero-teaser__text"> <h2 class="hero-teaser__headline" attr="data.headline.attributes" css="data.headline.css" html="data.headline.html"> </h2> <a class="btn" attr="data.link.attributes" html="data.link.html"> </a> </div> </div> </section> </div> </div> |
This file is used for rendering the form input into HTML and saving the result in the database. Now the element definition from the configuration file “hero_teaser.xml” comes into play. The template contains placeholders which look like “data.<element_name>.<attribute_name>” while the element names and attribute names are defined in the configuration file “hero_teaser.xml”. There is also a general attribute name “attributes” which contains all attributes of an element.
Into the database
Upon saving the CMS page / CMS block / product / …, the HTML will be rendered by JavaScript and saved in the database directly. This can look as follows:
Dynamic Content
A typical purpose of Page Builder elements is showing dynamic content which depends on the database. You can’t do that directly with just HTML. If you want to do that, you can insert a widget. Here is what it can look like in the master.html:
1 |
{{widget type="Namespace\Catalog\Block\Widget\CategoryList" template="Namespace_Catalog::widget/category_list.phtml" category_id="5"}} |
Exactly this mechanism is used for displaying product lists in the core Page Builder elements. You replicate the input via the UI component and pass them through to the widget.
Advanced Features
There are many more features which are not covered in this blog post:
- Images
- Other input types like selects
- Modifying the input with JavaScript
- Nesting different elements (i.e. for Sliders)
There are many examples in the core elements which you can copy.
Also, check out the official Developer Documentation which is the most complete documentation you will find.
Migration from Bluefoot to Page Builder
There is an official migration tool which will transform all default Bluefoot content elements to Page Builder. Please see the official developer documentation for its usage. The documentation also describes how to migrate custom content types.
Make sure to have this one condition fulfilled if you want to use the migration tool:
- The migration tool can only be used in Magento Commerce 2.3.1. It won’t work in 2.3.2 or newer.
This means that you have to migrate from Magento 2.2.x to 2.3.1 first, then migrate the content, and then update to Magento 2.3.3 or newer. Please plan accordingly.

Author: Andreas von Studnitz
Andreas von Studnitz is a Magento developer and one of the Managing Directors at integer_net. His main areas of interest are backend development, Magento consulting and giving developer trainings. He is a Magento 2 Certified Professional Developer Plus and holds several other Magento certifications for both Magento 1 and Magento 2. Andreas was selected as a Magento Master in 2019 and 2020.