Magento 2 Page Builder: Create a Custom Content Type
Page Builder is the drag and drop editor for CMS pages, blocks, categories and product descriptions. It was an Adobe Commerce feature first and has been included in Magento Open Source since 2.4.3, so on 2.4.8 both editions have it. It ships with rows, columns, text, images, banners, sliders and a few more. When editors need a component that is specific to your store, such as a styled quote, a USP list or a store locator teaser, you can add it as a custom content type.
This article walks through a small content type, a quote with an author, and explains each file. It is the same structure as Adobe's own Page Builder examples, and it scales to bigger content types.
How Page Builder stores content
This is the most important thing to understand before building anything. Page Builder does not store JSON. It stores HTML in the normal content field, with data attributes that describe each element:
<div data-content-type="vendor_quote" data-appearance="default" data-element="main">
<blockquote data-element="quote">Good tools make good work.</blockquote>
<cite data-element="author">Someone</cite>
</div>
When an editor opens the content, Page Builder reads this HTML back into its data model. When they save, it writes HTML again. Two consequences follow:
- The storefront renders stored HTML directly. A simple content type needs no PHP and no storefront JavaScript at all.
- The HTML your content type writes is a contract. Changing the structure later means old content may not read back correctly, so design it carefully up front.
Plan before you build
Because the stored HTML is hard to change later, spend a few minutes on a plan before writing XML. Write down the fields editors will fill, which of them are plain text and which need rich text, which style options they really need, and the exact HTML you want on the storefront. Look at the design for every breakpoint, not only desktop. Then check whether an existing content type with a CSS class from the advanced fieldset already covers it. Many requests for a new content type turn out to be a text element with a class, which costs nothing to maintain.
Files involved
app/code/Vendor/PageBuilderQuote/
registration.php
etc/module.xml
view/adminhtml/pagebuilder/content_type/vendor_quote.xml
view/adminhtml/ui_component/pagebuilder_vendor_quote_form.xml
view/adminhtml/web/template/content-type/vendor-quote/default/preview.html
view/adminhtml/web/template/content-type/vendor-quote/default/master.html
view/frontend/web/css/source/_module.less
Add Magento_PageBuilder to the sequence in module.xml, so your configuration loads after the core content types.
1. The content type configuration
<?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="vendor_quote"
label="Quote"
component="Magento_PageBuilder/js/content-type"
preview_component="Magento_PageBuilder/js/content-type/preview"
master_component="Magento_PageBuilder/js/content-type/master"
form="pagebuilder_vendor_quote_form"
menu_section="elements"
icon="icon-pagebuilder-heading"
sortOrder="50"
translate="label">
<children default_policy="deny"/>
<appearances>
<appearance name="default"
default="true"
preview_template="Vendor_PageBuilderQuote/content-type/vendor-quote/default/preview"
master_template="Vendor_PageBuilderQuote/content-type/vendor-quote/default/master"
reader="Magento_PageBuilder/js/master-format/read/configurable">
<elements>
<element name="main">
<style name="text_align" source="text_align"/>
<style name="margins" storage_key="margins_and_padding" reader="Magento_PageBuilder/js/property/margins" converter="Magento_PageBuilder/js/converter/style/margins"/>
<style name="padding" storage_key="margins_and_padding" reader="Magento_PageBuilder/js/property/paddings" converter="Magento_PageBuilder/js/converter/style/paddings"/>
<attribute name="name" source="data-content-type"/>
<attribute name="appearance" source="data-appearance"/>
<css name="css_classes"/>
</element>
<element name="quote">
<html name="quote_text" converter="Magento_PageBuilder/js/converter/html/tag-escaper"/>
</element>
<element name="author">
<html name="quote_author" converter="Magento_PageBuilder/js/converter/html/tag-escaper"/>
</element>
</elements>
</appearance>
</appearances>
</type>
</config>
What the main parts do:
- name is the identifier written to
data-content-type. Pick it once and keep it, since stored content refers to it. - component, preview_component and master_component point at the generic core classes. You only write your own preview component when the stage needs custom behaviour, such as a live preview that calls an API.
- form is the UI component form editors see when they edit the element.
- menu_section is the group in the left panel. Core uses
layout,elements,mediaandadd_content. - children with
default_policy="deny"stops editors from dropping other content types inside the quote. - elements map form fields to the HTML. Each
elementname matches adata-elementin the templates.stylewrites inline styles,attributewrites attributes,csswrites classes, andhtmlwrites inner HTML.
The tag-escaper converter escapes HTML tags in plain text fields, so an editor typing angle brackets does not break the markup. Use a WYSIWYG field and a different converter only when you really want rich text inside the element.
The margin and padding mapping above follows the pattern core content types use with the shared advanced fieldset. Open vendor/magento/module-page-builder/view/adminhtml/pagebuilder/content_type/heading.xml and compare. Copying from a core type of similar shape is the most reliable way to get these mappings right.
2. The edit form
The form is a regular admin UI component form that extends pagebuilder_base_form. The base form brings the shared appearance and advanced fieldsets, such as alignment, borders, margins, padding and CSS classes. The quickest safe start is to copy a small core form, for example pagebuilder_heading_form.xml from vendor/magento/module-page-builder/view/adminhtml/ui_component/, rename the namespace and data source to match your file name, and replace its fields with yours:
<fieldset name="general" sortOrder="20">
<settings>
<label/>
</settings>
<field name="quote_text" sortOrder="10" formElement="textarea">
<settings>
<dataScope>quote_text</dataScope>
<dataType>text</dataType>
<label translate="true">Quote</label>
<validation>
<rule name="required-entry" xsi:type="boolean">true</rule>
</validation>
</settings>
</field>
<field name="quote_author" sortOrder="20" formElement="input">
<settings>
<dataScope>quote_author</dataScope>
<dataType>text</dataType>
<label translate="true">Author</label>
</settings>
</field>
</fieldset>
The dataScope values must match the name attributes in the html nodes of the content type XML. A mismatch is the most common reason a field saves but never appears on the stage.
If you are new to admin UI component forms, the UI components basics explain the underlying framework.
3. Preview and master templates
Each appearance has two Knockout templates. The preview template renders on the Page Builder stage in the admin, with the options menu that lets editors move, edit, duplicate and remove the element. The master template produces the HTML that is saved and shown on the storefront.
preview.html:
<div attr="data.main.attributes"
ko-style="data.main.style"
css="data.main.css"
class="pagebuilder-content-type"
event="{ mouseover: onMouseOver, mouseout: onMouseOut }, mouseoverBubble: false">
<render args="getOptions().template"/>
<blockquote attr="data.quote.attributes" ko-style="data.quote.style" css="data.quote.css" html="data.quote.html"></blockquote>
<cite attr="data.author.attributes" ko-style="data.author.style" css="data.author.css" html="data.author.html"></cite>
</div>
master.html:
<div attr="data.main.attributes" ko-style="data.main.style" css="data.main.css">
<blockquote attr="data.quote.attributes" ko-style="data.quote.style" css="data.quote.css" html="data.quote.html"></blockquote>
<cite attr="data.author.attributes" ko-style="data.author.style" css="data.author.css" html="data.author.html"></cite>
</div>
Keep the master template free of editor only markup and event handlers. Anything in it ends up in the stored content and on the storefront.
4. Storefront styling
Since the storefront only sees stored HTML, styling is plain CSS targeting the data attributes:
[data-content-type='vendor_quote'] blockquote {
font-size: 1.5rem;
font-style: italic;
margin: 0 0 0.5rem;
}
[data-content-type='vendor_quote'] cite {
display: block;
font-style: normal;
opacity: 0.8;
}
Put admin styles in the adminhtml area as well, so the stage looks close to the storefront. Editors trust what they see on the stage.
If the content type needs storefront behaviour, such as a slider, add a widget initialiser for it in the frontend area. Core content types like the slider do this. Keep it lazy, because the script loads wherever the content appears.
Deploying and testing
bin/magento setup:upgrade
bin/magento cache:flush
bin/magento setup:static-content:deploy -f --area adminhtml
Then test the full cycle: add the element, fill the form, save, reload the page in the admin and check that the values read back, and check the storefront output. Reading back after save is the step that catches mapping mistakes.
Pitfalls
- Removing the module leaves content behind. Stored HTML still renders on the storefront, but Page Builder can no longer edit those elements. Plan migrations of content before removing a content type.
- Changing element names breaks old content. Add new elements rather than renaming existing ones.
- Dynamic data does not belong here. A content type stores static HTML. For content that must change without editing, such as prices or stock, combine it with a custom widget that renders server side.
If you are deciding between editions for other reasons, see Magento Open Source vs Adobe Commerce. Page Builder itself is no longer a reason to choose one over the other, although content staging and previews of scheduled changes remain Adobe Commerce features.
FAQ
Is Page Builder available in Magento Open Source?
Yes. It has been bundled with Magento Open Source since 2.4.3. Content staging, scheduled updates and some preview features remain Adobe Commerce only.
Do I need PHP code for a custom content type?
Not for a simple one. The configuration XML, the form and two templates are enough, because the storefront renders the stored HTML directly.
Why does my field save but not show on the stage?
The dataScope in the form does not match the name in the content type XML, or the element name in the XML does not match the data-element in the templates.
Can I add a new appearance to a core content type?
Yes. Declare the same type name in your own content_type XML with a new appearance node and templates, and Magento merges it with the core configuration.
I am available for freelance Magento and Adobe Commerce work, including Page Builder content types for content teams. If you need help with this, get in touch.
Related articles
Magento 2 Cart Price Rules: Add a Custom Condition
Add a custom condition to Magento 2 cart price rules: the combine event, a condition model with validate, promo rule attributes and performance traps to avoid.
Magento 2 Custom Fee: Write a Totals Collector That Works
Add a custom fee to Magento 2 with a quote totals collector: sales.xml, collect and fetch, cart and checkout display, copying it to orders, invoices and PDFs.
Magento 2 Custom Payment Method with the Payment Gateway API
Build a custom payment method in Magento 2.4 using the Payment Gateway API: facade virtual type, config, commands and the checkout JS renderer, step by step.
Magento 2 Custom Shipping Method: Build a Carrier Module
How to build a custom shipping method in Magento 2.4: config.xml, system.xml, a carrier model with collectRates, free shipping rules and external rate APIs.