Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

Einen eigenen "Punkte-Banner" Content Type für Page Builder bauen

Einen eigenen "Punkte-Banner" Content Type für Page Builder bauen

~9 Min. Lesezeit Zuletzt aktualisiert am 9. August 2026

Kapitel 58 hat die Grenze klar gezogen: ein eigener Content Type zeigt in der Regel statischen, redaktionellen Inhalt, keinen live-aktuellen Punktestand. Der "Punkte-Banner" dieses Kapitels nimmt das ernst - er wirbt FÜR das Treueprogramm ("Sammle Punkte bei jedem Einkauf!"), zeigt aber niemals den Punktestand eines konkreten Kunden. Frei per Drag-and-drop auf jeder Landingpage platzierbar, mit Überschrift, Text, Call-to-Action und Hintergrundfarbe, alles im Admin editierbar.

Content-Type-Registrierung

app/code/Mironsoft/Loyalty/etc/pagebuilder/content_type.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="mironsoft_loyalty_points_banner"
          label="Points Banner"
          translate="label"
          component="Magento_PageBuilder/js/content-type"
          preview_component="Mironsoft_Loyalty/js/content-type/points-banner/preview"
          master_component="Magento_PageBuilder/js/content-type/master"
          form="mironsoft_loyalty_points_banner_form"
          icon="icon-pagebuilder-banner"
          menu_section="elements"
          sortOrder="200">
        <appearances>
            <appearance name="default" default="true"/>
        </appearances>
    </type>
</config>

Tipp: Diese Datei zeigt die wesentliche Form, nicht das komplette XSD - siehe den Tipp am Ende von Kapitel 58 zum eingebauten Banner-Content-Type als vollständigem Referenzbeispiel in der eigenen Installation.

Admin-Formular für die Eigenschaften

form="mironsoft_loyalty_points_banner_form" verweist auf eine UI-Component-Formulardefinition mit den fünf Feldern heading, body_text, cta_label, cta_url und background_color - technisch dieselbe UI-Component-Formular-Technik, die bereits das Prämien-Admin-Grid aus Kapitel 16 genutzt hat. Die separate Tutorial-Serie "Admin-Grids & Formulare in Magento 2" dieses Katalogs (admin-grids-formulare DE / magento-admin-grids-forms EN) behandelt UI-Component-Formulare im Detail - hier reicht die Feldliste, um zu verstehen, was PointsBanner::getHeading() & Co. unten lesen.

Die Content-Type-Klasse

app/code/Mironsoft/Loyalty/Block/PageBuilder/PointsBanner.php
<?php

declare(strict_types=1);

namespace Mironsoft\Loyalty\Block\PageBuilder;

use Magento\Framework\View\Element\Template\Context;
use Magento\PageBuilder\Block\ContentType\ContentTypeAbstract;

/**
 * Renders the "Points Banner" Page Builder content type: a static, admin-editable marketing
 * banner promoting the loyalty program. Deliberately holds no dependency on
 * ViewModel\PointsBalance (chapter 48) or PointsBalanceWidget (chapter 56) - unlike those,
 * this content type shows the exact same content to every visitor, so it needs no
 * per-customer data at all (chapter 58 explains why that distinction matters here).
 * Extends the same base class Magento's own Banner content type extends.
 */
class PointsBanner extends ContentTypeAbstract
{
    /**
     * @param Context $context Framework template context.
     * @param array<string, mixed> $data Additional block data, populated by Page Builder from the saved attributes.
     */
    public function __construct(
        Context $context,
        array $data = [],
    ) {
        parent::__construct($context, $data);
    }

    /**
     * Returns the configured banner heading, falling back to a sensible default.
     *
     * @return string
     */
    public function getHeading(): string
    {
        return (string) ($this->getData('heading') ?: __('Earn points on every order'));
    }

    /**
     * Returns the configured banner body text, empty string if the editor left it blank.
     *
     * @return string
     */
    public function getBodyText(): string
    {
        return (string) $this->getData('body_text');
    }

    /**
     * Returns the configured call-to-action label, empty string if unset.
     *
     * @return string
     */
    public function getCtaLabel(): string
    {
        return (string) $this->getData('cta_label');
    }

    /**
     * Returns the configured call-to-action target URL, empty string if unset.
     *
     * @return string
     */
    public function getCtaUrl(): string
    {
        return (string) $this->getData('cta_url');
    }

    /**
     * Returns the configured background color as a CSS-safe hex value, defaulting to the
     * brand-dark color when the stored value is missing or malformed.
     *
     * @return string
     */
    public function getBackgroundColor(): string
    {
        $color = (string) $this->getData('background_color');

        return $color !== '' && preg_match('/^#[0-9a-fA-F]{6}$/', $color) === 1 ? $color : '#0f172a';
    }
}

\Magento\PageBuilder\Block\ContentType\ContentTypeAbstract ist die von Page Builder vorgegebene Basisklasse, die Kapitel 47 bereits als dritten Fall angekündigt hat, in dem Magentos Kernkonventionen eine echte Block-Unterklasse erzwingen - genau dieselbe Klasse, von der auch der eingebaute Banner-Content-Type erbt.

Das Frontend-Template

app/code/Mironsoft/Loyalty/view/frontend/templates/pagebuilder/points-banner/default.phtml
<?php

declare(strict_types=1);

use Magento\Framework\Escaper;
use Mironsoft\Loyalty\Block\PageBuilder\PointsBanner;

/**
 * @var PointsBanner $block
 * @var Escaper $escaper
 */
?>
<div class="my-8 overflow-hidden rounded-2xl px-6 py-10 text-center sm:px-12"
     style="background-color: <?= $escaper->escapeHtmlAttr($block->getBackgroundColor()) ?>;">
    <h2 class="text-2xl font-bold text-white sm:text-3xl">
        <?= $escaper->escapeHtml($block->getHeading()) ?>
    </h2>
    <?php if ($block->getBodyText() !== ''): ?>
        <p class="mx-auto mt-3 max-w-2xl text-base text-white/80">
            <?= $escaper->escapeHtml($block->getBodyText()) ?>
        </p>
    <?php endif; ?>
    <?php if ($block->getCtaLabel() !== '' && $block->getCtaUrl() !== ''): ?>
        <a href="<?= $escaper->escapeUrl($block->getCtaUrl()) ?>"
           class="mt-6 inline-flex items-center rounded-full bg-white px-6 py-3 text-sm font-semibold text-gray-900 shadow-sm hover:bg-gray-100">
            <?= $escaper->escapeHtml($block->getCtaLabel()) ?>
        </a>
    <?php endif; ?>
</div>

Dasselbe Block+Template-Paar übernimmt zwei Rollen: es erzeugt beim Speichern der Seite im Admin das eingefrorene Master-Format-HTML (Kapitel 58) UND liefert - sollte ein Content Type das je erfordern - dieselbe Ausgabe bei direktem Frontend-Rendering. Für einen rein statischen Content Type wie diesen ist das ein Implementierungsdetail ohne sichtbaren Unterschied; wichtig wird es erst bei Content Types, die den {{widget}}-Platzhalter-Trick aus Kapitel 58 nutzen.

Achtung: Bewusst KEINE Verbindung zu ViewModel\PointsBalance oder PointsBalanceWidget. Ein Versuch, hier den eingeloggten Kundennamen oder Punktestand einzublenden, würde entweder denselben (den der Redakteurin beim Speichern) Wert für ALLE Besucher zeigen oder - je nach Master-Format-Erzeugung - gar keinen sinnvollen Wert, niemals den tatsächlich aktuellen Wert der jeweiligen Besucherin. Genau das ist der in Kapitel 58 beschriebene strukturelle Unterschied zum Widget, nicht ein vermeidbarer Implementierungsfehler.

Tipp: Wie widget.xml in Kapitel 55: content_type.xml landet im config-Cache. bin/cache-clean config nach jeder Änderung, damit der neue Content Type in der Page-Builder-Bausteinpalette im Admin auftaucht.