PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

PageBuilder – własny typ zawartości, XML config

by Henryk Tews / wtorek, 11 lipca 2023 / Opublikowano w Magento 2

PageBuilder to wizualny edytor stron wbudowany w Magento 2.3.1+. Klienci go kochają – przeciągają bloki, edytują treści bez HTML. Developerzy mają mieszane uczucia – architektura jest złożona, debugowanie nieintuicyjne. Pokazuję jak dodać własny typ zawartości od zera, jak rozszerzyć istniejące typy i czego unikać przy wdrożeniu.

Architektura PageBuilder

PageBuilder działa na trzech warstwach:

  • PHP Backend – konwersja między HTML przechowanym w bazie a strukturą JSON dla edytora
  • JavaScript/knockout.js – sam edytor, preview w czasie rzeczywistym, drag-and-drop
  • Storefront renderowanie – widgets i HTML generowany z atrybutów data- na stronie

Dane są przechowywane jako HTML z atrybutami data-content-type, data-appearance i data-element. PageBuilder parsuje ten HTML na drzewo obiektów po stronie JS.

Struktura własnego typu zawartości

Vendor/PageBuilderModule/
  etc/
    module.xml
  view/
    adminhtml/
      web/
        js/
          content-type/
            product-banner/
              appearance/
                default/
                  master.html      <- szablon podglądu w edytorze
                  preview.html     <- szablon live preview
              widget.html          <- szablon frontendu
        css/
          source/
            content-type/
              _product-banner.less
    base/
      pagebuilder/
        content_type/
          product_banner.xml       <- konfiguracja typu
  registration.php

Konfiguracja typu zawartości – XML

<!-- view/base/pagebuilder/content_type/product_banner.xml -->
<?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_banner"
          label="Product Banner"
          menu_section="media"
          component="Magento_PageBuilder/js/content-type"
          preview_component="Vendor_PageBuilderModule/js/content-type/product-banner/preview"
          master_component="Magento_PageBuilder/js/content-type/master"
          form="pagebuilder_product_banner_form"
          icon="icon-pagebuilder-image"
          sortOrder="35"
          translate="label">

        <children default_policy="deny"/>

        <appearances>
            <appearance name="default"
                        default="true"
                        preview_template="Vendor_PageBuilderModule/content-type/product-banner/default/preview"
                        master_template="Vendor_PageBuilderModule/content-type/product-banner/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"/>
                        <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="name" source="data-content-type"/>
                        <attribute name="appearance" source="data-appearance"/>
                        <attribute name="product_sku" source="data-product-sku"/>
                        <attribute name="show_price" source="data-show-price"/>
                        <css name="css_classes"/>
                    </element>
                </elements>
            </appearance>
        </appearances>
    </type>

</config>

Formularz edytora – UI Component

<!-- view/adminhtml/ui_component/pagebuilder_product_banner_form.xml -->
<?xml version="1.0"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">

    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">pagebuilder_product_banner_form.pagebuilder_product_banner_form_data_source</item>
        </item>
        <item name="label" xsi:type="string" translate="true">Product Banner</item>
    </argument>

    <settings>
        <deps>
            <dep>pagebuilder_product_banner_form.pagebuilder_product_banner_form_data_source</dep>
        </deps>
        <namespace>pagebuilder_product_banner_form</namespace>
    </settings>

    <dataSource name="pagebuilder_product_banner_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>
    </dataSource>

    <fieldset name="appearance_fieldset"
              component="Magento_PageBuilder/js/form/element/dependent-fieldset">
        <settings>
            <label translate="true">Appearance</label>
            <collapsible>false</collapsible>
            <opened>true</opened>
        </settings>

        <field name="appearance" formElement="select"
               component="Magento_PageBuilder/js/form/element/appearance">
            <settings>
                <label translate="true">Appearance</label>
            </settings>
        </field>
    </fieldset>

    <fieldset name="general">
        <settings>
            <label translate="true">Product Banner Settings</label>
        </settings>

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

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

</form>

Preview komponent JavaScript

// view/adminhtml/web/js/content-type/product-banner/preview.js
define([
    'Magento_PageBuilder/js/content-type/preview',
    'jquery',
    'ko'
], function (Preview, $, ko) {
    'use strict';

    return Preview.extend({
        productData: ko.observable(null),
        loading: ko.observable(false),

        /**
         * Inicjalizacja - pobierz dane produktu z Magento API
         */
        initialize: function () {
            this._super();

            // Obserwuj zmiany SKU w formularzu
            this.contentType.dataStore.subscribe(
                function (data) {
                    if (data.product_sku) {
                        this.loadProductData(data.product_sku);
                    }
                }.bind(this)
            );
        },

        loadProductData: function (sku) {
            this.loading(true);

            $.ajax({
                url: '/rest/V1/products/' + encodeURIComponent(sku),
                type: 'GET',
                headers: {
                    'Authorization': 'Bearer ' + window.adminToken
                },
            })
            .done(function (product) {
                this.productData({
                    name:  product.name,
                    price: product.price,
                    image: this.getProductImage(product)
                });
            }.bind(this))
            .fail(function () {
                this.productData(null);
            }.bind(this))
            .always(function () {
                this.loading(false);
            }.bind(this));
        },

        getProductImage: function (product) {
            var mediaAttr = product.custom_attributes
                ? product.custom_attributes.find(a => a.attribute_code === 'thumbnail')
                : null;

            return mediaAttr
                ? '/media/catalog/product' + mediaAttr.value
                : '/pub/static/frontend/Magento/luma/pl_PL/Magento_Catalog/images/product/placeholder/thumbnail.jpg';
        }
    });
});

Szablon podglądu w edytorze

<!-- view/adminhtml/web/template/content-type/product-banner/default/preview.html -->
<div class="pagebuilder-product-banner" attr="data.main.attributes" ko-style="data.main.style" css="data.main.css">
    <!-- Loading indicator -->
    <div if="preview.loading" class="product-banner-loading">
        <span>Ładowanie produktu...</span>
    </div>

    <!-- Podgląd z danymi produktu -->
    <div ifnot="preview.loading">
        <div if="preview.productData">
            <div class="product-banner-image">
                <img attr="{src: preview.productData().image, alt: preview.productData().name}"/>
            </div>
            <div class="product-banner-info">
                <h3 text="preview.productData().name"></h3>
                <p if="data.main.attributes['data-show-price'] === 'true'"
                   class="product-banner-price"
                   text="'$' + preview.productData().price"></p>
            </div>
        </div>

        <!-- Placeholder gdy brak SKU -->
        <div ifnot="preview.productData" class="product-banner-placeholder">
            <span>Wybierz produkt po SKU</span>
        </div>
    </div>

    <!-- Toolbar edytora (kopiuj, przesuń, usuń) -->
    <render args="getOptions().template"/>
</div>

Renderowanie na frontendzie

<?php

// Block PHP dla widgetu na frontendzie
namespace Vendor\PageBuilderModule\Block\ContentType;

use Magento\Framework\View\Element\Template;
use Magento\Catalog\Api\ProductRepositoryInterface;

class ProductBanner extends Template
{
    public function __construct(
        Template\Context $context,
        private ProductRepositoryInterface $productRepository,
        array $data = []
    ) {
        parent::__construct($context, $data);
    }

    public function getProduct(): ?\Magento\Catalog\Api\Data\ProductInterface
    {
        $sku = $this->getData('product_sku');

        if (!$sku) {
            return null;
        }

        try {
            return $this->productRepository->get($sku);
        } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
            return null;
        }
    }

    public function shouldShowPrice(): bool
    {
        return (bool) $this->getData('show_price');
    }
}

Najczęstsze pułapki przy PageBuilder

Kilka rzeczy które nauczyły mnie boleśnie przy wdrożeniach PageBuilder:

  • HTML w bazie danych - PageBuilder przechowuje HTML z atrybutami data- bezpośrednio w polach tekstowych. Migracje treści przy aktualizacji modułu wymagają parsowania i transformacji tego HTML.
  • Wydajność podglądu - preview robi live request do API przy każdej zmianie w formularzu. Dodaj debounce lub cache po stronie JS żeby nie zalewać backendu requestami.
  • Wersjonowanie szablonów - HTML przechowany w bazie jest mocno sprzężony ze strukturą szablonu. Zmiana atrybutów data- w szablonie może zepsuć istniejące treści.
  • Testowanie - PageBuilder jest trudny do testowania jednostkowego przez silne sprzężenie z UI. Skup się na testach integracyjnych walidujących output HTML.

Podsumowanie

PageBuilder to potężne narzędzie które daje klientom dużą swobodę edycji treści. Własny typ zawartości to kilka plików – XML, JS, HTML – ale schema i architektura są precyzyjne i wymagają dokładnego przestrzegania konwencji. Zanim zaczniesz pisać własny typ, przejrzyj kod natywnych typów Magento jak Banner, Row czy Column – to najlepsze wzorce do naśladowania.

About Henryk Tews

Co możesz przeczytać następne

Cron – grupy, własne joby, harmonogram z panelu admina, debugowanie
PWA Studio vs Hyvä vs Luma – rzetelne porównanie TCO i kiedy który
Akeneo PIM – po co PIM, REST API, mapowanie atrybutów, import przez cron
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
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 Zawsze aktywne
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.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}