Building a Custom "Points Banner" Content Type for Page Builder
Building a Custom "Points Banner" Content Type for Page Builder
~9 Min. Lesezeit Zuletzt aktualisiert am August 9, 2026
Chapter 58 drew a clear line: a custom content type usually shows static, editorial content, not a live points balance. This chapter's "Points Banner" takes that seriously - it promotes the loyalty program itself ("Earn points on every order!"), but never shows any individual customer's balance. Freely placeable via drag and drop on any landing page, with a heading, body text, call-to-action, and background color, all editable in the admin.
Content type registration
<?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: This file shows the essential shape, not the complete XSD - see the tip at the end of chapter 58 about the built-in Banner content type as a complete reference example in your own installation.
Admin form for the properties
form="mironsoft_loyalty_points_banner_form" points at a UI component form definition with the five fields heading, body_text, cta_label, cta_url, and background_color - technically the same UI component form technique the reward admin grid already used back in chapter 16. This catalog's separate "Admin Grids & Forms in Magento 2" tutorial series (admin-grids-formulare DE / magento-admin-grids-forms EN) covers UI component forms in detail - the field list here is enough to understand what PointsBanner::getHeading() and friends read below.
The content type class
<?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 is the base class Page Builder itself dictates, already announced back in chapter 47 as the third case where Magento's core conventions force a real block subclass - the exact same class the built-in Banner content type extends too.
The frontend template
<?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>The same block+template pair plays two roles: it produces the frozen master format HTML when the page is saved in the admin (chapter 58) AND - should a content type ever require it - serves the same output for direct frontend rendering. For a purely static content type like this one, that's an implementation detail with no visible difference; it only starts mattering for content types that use the {{widget}} placeholder trick from chapter 58.
Achtung: Deliberately NO connection to ViewModel\PointsBalance or PointsBalanceWidget. Trying to show the logged-in customer's name or points balance here would either show the same value (the editor's, at save time) to EVERY visitor, or - depending on how the master format was generated - no meaningful value at all, never the actual current value for the visitor looking at the page. That's exactly the structural difference from a widget described in chapter 58, not an avoidable implementation mistake.
Tipp: Just like widget.xml in chapter 55: content_type.xml lands in the config cache. bin/cache-clean config after every change, so the new content type shows up in Page Builder's block palette in the admin.