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.
