PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PageBuilder – custom content type, XML config, JS preview, frontend rendering

by Henryk Tews / Tuesday, 11 July 2023 / Published in Magento 2

PageBuilder is Magento 2’s drag-and-drop content editor that replaced the plain HTML textarea in admin. A custom content type lets merchants create complex layouts without HTML knowledge while giving developers full control over the output. I show the complete implementation: XML configuration, JavaScript preview in admin, and frontend rendering.

PageBuilder architecture

A custom content type consists of four layers:

  • config.xml – declares the type, fields, and appearance
  • Preview JS component – how the block looks in admin’s PageBuilder editor
  • Master/data form – what fields appear in the side panel when editing
  • Frontend template (PHTML) – how the block is rendered on the storefront

Module structure

Vendor/PageBuilderExtension/
  etc/
    module.xml
    config.xml                          <- content type config
  view/
    adminhtml/
      web/
        js/content-type/
          product-showcase/
            appearance/
              default/
                preview.js             <- admin preview
        template/
          product-showcase/
            default/
              master.html              <- saved HTML master format
              preview.html             <- admin preview template
    base/
      ui_component/
        pagebuilder_product_showcase_form.xml  <- edit form
    frontend/
      templates/
        content-type/
          product-showcase/
            default.phtml             <- storefront rendering

config.xml – declare the content type

<?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="product_showcase"
          label="Product Showcase"
          component="Magento_PageBuilder/js/content-type"
          preview_component="Vendor_PageBuilderExtension/js/content-type/product-showcase/appearance/default/preview"
          master_component="Magento_PageBuilder/js/content-type/master"
          form="pagebuilder_product_showcase_form"
          menu_section="content"
          icon="icon-pagebuilder-product"
          sortOrder="40"
          translate="label">

        <children default_policy="deny"/>

        <appearances>
            <appearance name="default"
                        default="true"
                        preview_template="Vendor_PageBuilderExtension/content-type/product-showcase/default/preview"
                        master_template="Vendor_PageBuilderExtension/content-type/product-showcase/default/master"
                        reader="Magento_PageBuilder/js/master-format/read/configurable">
                <elements>
                    <element name="main">
                        <style name="text_align"     source="text_align"/>
                        <style name="border"         source="border_style" converter="Magento_PageBuilder/js/converter/style/border-style"/>
                        <style name="border_color"   source="border_color"/>
                        <style name="border_width"   source="border_width" converter="Magento_PageBuilder/js/converter/style/border-width"/>
                        <style name="border_radius"  source="border_radius" converter="Magento_PageBuilder/js/converter/style/border-radius"/>
                        <attribute name="data-content-type" source="name"/>
                        <attribute name="data-appearance"   source="appearance"/>
                        <attribute name="data-sku"          source="sku"/>
                        <attribute name="data-show-price"   source="show_price"/>
                        <attribute name="data-button-label" source="button_label"/>
                    </element>
                </elements>
            </appearance>
        </appearances>
    </type>
</config>

Edit form – UI Component

<!-- view/adminhtml/ui_component/pagebuilder_product_showcase_form.xml -->
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <fieldset name="appearance_fieldset"
              component="Magento_PageBuilder/js/form/element/dependent-fieldset">
        <field name="appearance" formElement="select" component="Magento_PageBuilder/js/form/element/appearance">
            <settings>
                <dataType>text</dataType>
                <label translate="true">Appearance</label>
            </settings>
        </field>
    </fieldset>

    <fieldset name="product_fieldset">
        <settings><label translate="true">Product</label></settings>

        <field name="sku" formElement="input">
            <settings>
                <dataType>text</dataType>
                <label translate="true">Product SKU</label>
                <validation><rule name="required-entry" xsi:type="boolean">true</rule></validation>
            </settings>
        </field>

        <field name="show_price" formElement="checkbox">
            <settings>
                <dataType>boolean</dataType>
                <label translate="true">Show Price</label>
            </settings>
        </field>

        <field name="button_label" formElement="input">
            <settings>
                <dataType>text</dataType>
                <label translate="true">Button Label</label>
            </settings>
        </field>
    </fieldset>
</form>

Admin preview JS component

// view/adminhtml/web/js/content-type/product-showcase/appearance/default/preview.js
define([
    'Magento_PageBuilder/js/content-type/preview',
    'Magento_PageBuilder/js/events',
    'mage/translate',
], function (PreviewBase, events, $t) {
    'use strict';

    return PreviewBase.extend({
        defaults: {
            productData: null,
            isLoading: true,
        },

        initialize: function () {
            this._super();

            // Watch for SKU changes and refresh the preview
            this.contentType.dataStore.subscribe((state) => {
                if (state.sku) {
                    this.loadProductPreview(state.sku);
                }
            }, 'sku');
        },

        loadProductPreview: function (sku) {
            this.isLoading(true);
            fetch(`/rest/V1/products/${sku}`)
                .then(r => r.json())
                .then(product => {
                    this.productData(product);
                    this.isLoading(false);
                })
                .catch(() => {
                    this.productData(null);
                    this.isLoading(false);
                });
        },
    });
});

Frontend template – PHTML

<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var \Magento\Framework\Escaper $escaper */
$sku       = $escaper->escapeHtmlAttr($block->getData('sku') ?? '');
$showPrice = (bool) ($block->getData('show_price') ?? false);
$btnLabel  = $escaper->escapeHtml($block->getData('button_label') ?? 'Add to Cart');

$objectManager = \Magento\Framework\App\ObjectManager::getInstance();
$productRepo   = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class);

try {
    $product = $productRepo->get($sku);
} catch (\Exception $e) {
    return; // SKU not found - render nothing
}
?>
<div class="product-showcase"
     data-content-type="product_showcase"
     data-appearance="default">
    <div class="product-showcase__image">
        <img src="<?= $escaper->escapeUrl(
            $block->getUrl('catalog/product/image', ['image' => $product->getImage()])
        ) ?>" alt="<?= $escaper->escapeHtmlAttr($product->getName()) ?>" />
    </div>
    <div class="product-showcase__info">
        <h3><?= $escaper->escapeHtml($product->getName()) ?></h3>
        <?php if ($showPrice): ?>
            <p class="price"><?= $escaper->escapeHtml(
                number_format((float)$product->getPrice(), 2, ',', ' ') . ' PLN'
            ) ?></p>
        <?php endif; ?>
        <a href="<?= $escaper->escapeUrl($product->getProductUrl()) ?>"
           class="action primary">
            <?= $escaper->escapeHtml($btnLabel) ?>
        </a>
    </div>
</div>

Summary

PageBuilder custom content types have a significant XML and JavaScript overhead upfront. Once the pattern is established, adding new fields is straightforward. The key parts are: config.xml for field-to-attribute mapping, the JS preview for admin UX, and the PHTML template for frontend. The biggest pitfall is the preview component – invest time here because it is what merchants interact with daily in the editor.

About Henryk Tews

What you can read next

Strategy pattern in PHP – and how Magento 2 uses it in pricing
Xdebug – configuration, PHPStorm, debugging Magento plugins

© 2026 Created by

TOP
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Always active
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
Zobacz preferencje
  • {title}
  • {title}
  • {title}