Building and Styling Hyvä Page Builder Content Types
AI generated
Hyvä
phtml
Hyvä · Page Builder · Content Types · Magento 2
Building and Styling Hyvä Page Builder Content Types
from the master format to the Alpine.js component

Custom Hyvä Page Builder content types are not built with Knockout widgets, they are rendered server side with phtml and Tailwind classes, plus targeted Alpine.js interactivity. This guide covers registration, rendering architecture and performance patterns with real code examples.

18 min read content_type.xml · appearance.xml · Alpine.js · Tailwind CSS Magento 2.4.x · Hyvä 1.3+ · Page Builder

1. Why Page Builder plays a special role in Hyvä

In Luma, Page Builder content is rendered on the client through Knockout.js widgets: every content type ships its own RequireJS component that translates the stored master format into DOM elements in the browser. That means extra JavaScript bundles, Knockout bindings and a rendering chain that directly contradicts the technologies Hyvä deliberately avoids. Hyvä Page Builder content types solve this problem at the architectural level: the hyva-themes/magento2-page-builder compatibility module replaces the entire client side rendering pipeline with server side PHP rendering.

For editors nothing changes in the admin editor, they keep using the familiar drag and drop interface from Magento_PageBuilder. The real difference is on the frontend: instead of hydrating Knockout components, Hyvä reads the stored content tree on the server and renders every node through its own phtml template. That turns Hyvä Page Builder content types into fully fledged, CSP compliant parts of the theme, without loading any extra bundle JavaScript or jQuery dependencies.

2. Architecture: a master format renderer instead of Knockout widget rendering

Page Builder stores content in the so called master format, an HTML structure with data-content-type attributes, nested containers and style information. In Luma, a client side converter picks up this structure and binds a Knockout component for every node. With Hyvä Page Builder content types, a PHP renderer walks the entire tree instead: it traverses the structure tree once on the server, resolves the matching template for each node type and renders the result directly into the page.

This architectural choice has tangible consequences. There is no runtime hydration in the browser, no extra network requests for widget definitions and no layout shift from components that get attached afterward. The mapping from content type name to template happens declaratively through content_type.xml, which means Hyvä Page Builder content types are embedded in the regular Hyvä rendering cycle just like normal block templates, and can be styled with Tailwind classes like any other theme fragment.

3. Registering a custom content type

A new content type needs at least two declaration files: content_type.xml defines the name, label, icon and available appearances, while appearance.xml defines the form fields editors fill in inside the Page Builder editor. Both files follow the same declarative pattern as other Magento configuration files and live per module under etc/pagebuilder/.


<!-- app/code/Mironsoft/PageBuilder/etc/pagebuilder/content_type.xml -->
<?xml version="1.0"?>
<content_types xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="urn:magento:module:Hyva_PageBuilder:etc/pagebuilder/content_type.xsd">
    <type name="feature-callout" label="Feature Callout" icon="icon-feature-callout" component="Magento_PageBuilder/js/content-type">
        <appearances>
            <appearance name="default" default="true"
                template="Mironsoft_PageBuilder::content-type/feature-callout/default.phtml"/>
            <appearance name="highlighted"
                template="Mironsoft_PageBuilder::content-type/feature-callout/highlighted.phtml"/>
        </appearances>
    </type>
</content_types>

<!-- app/code/Mironsoft/PageBuilder/etc/pagebuilder/feature-callout/appearance.xml -->
<?xml version="1.0"?>
<appearances xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_PageBuilder:etc/appearance.xsd">
    <appearance name="default">
        <elements>
            <element name="headline">
                <converter component="Magento_PageBuilder/js/content-type/text/converter" property="headline"/>
            </element>
            <element name="text">
                <converter component="Magento_PageBuilder/js/content-type/text/converter" property="text"/>
            </element>
            <element name="background_color">
                <converter component="Magento_PageBuilder/js/converter/style-attribute" property="background-color"/>
            </element>
        </elements>
    </appearance>
</appearances>

The template attribute value in content_type.xml is the crucial Hyvä specific addition: it points to the phtml template the PHP renderer uses for this appearance. Without this entry Hyvä falls back to a generic template that outputs the form field values unformatted. Anyone registering Hyvä Page Builder content types cleanly should also use a distinct module namespace in the name attribute, so there is no collision with core content types such as banner or row.

4. Building a rendering template in the Hyvä style

The actual rendering template is a regular phtml template that receives the field values stored in the master format as an array. The key difference from a generic Page Builder theme: instead of core CSS classes like pagebuilder-content-type or Bootstrap grid classes, Hyvä Page Builder content types use nothing but Tailwind utility classes that match the theme's existing design system.


<?php
/** @var \Magento\Framework\View\Element\Template $block */
/** @var array $data */
// $data is provided by the Hyvä PageBuilder renderer with all master-format field values
$data = $block->getData('content_type_data') ?? [];
$headline = (string) ($data['headline'] ?? '');
$text = (string) ($data['text'] ?? '');
$backgroundColor = (string) ($data['background_color'] ?? '#0f172a');
?>
<div class="not-prose rounded-2xl p-6 sm:p-8 my-6" style="background-color: <?= $block->escapeHtmlAttr($backgroundColor) ?>;">
    <?php if ($headline !== ''): ?>
    <p class="text-white text-xl font-bold mb-2"><?= $block->escapeHtml($headline) ?></p>
    <?php endif; ?>
    <?php if ($text !== ''): ?>
    <div class="text-white text-sm prose prose-invert max-w-none">
        <?= /* @noEscape */ $text ?>
    </div>
    <?php endif; ?>
</div>

It also pays off to reuse the spacing and typography scales from the Tailwind configuration instead of defining custom pixel values. That way Hyvä Page Builder content types stay visually consistent with the rest of the theme, even when editors freely combine content blocks. HTML fields such as text must be output deliberately unescaped since they already contain formatted, editorially produced HTML, while plain string fields always go through escapeHtml.

5. Adding Alpine.js interactivity to custom content types

As soon as a content type needs more than static markup, for example tabs or an accordion, Alpine.js comes into play, since it is loaded globally in Hyvä anyway. No extra bundle, no separate RequireJS module: the interactivity is declared directly as an x-data attribute in the template, which keeps Hyvä Page Builder content types interactive without violating the CSP configuration.


<?php
/** @var \Mironsoft\PageBuilder\Block\ContentType\Tabs $block */
// Tabs are stored as repeatable child elements in the master format
$tabs = $block->getTabs(); // array<int, array{label: string, html: string}>
?>
<div class="not-prose my-6" x-data="{ active: 0 }">
    <div class="flex flex-wrap gap-2 border-b border-slate-200 mb-4">
        <?php foreach ($tabs as $index => $tab): ?>
        <button
            type="button"
            @click="active = <?= (int) $index ?>"
            :class="active === <?= (int) $index ?> ? 'border-orange-600 text-orange-700' : 'border-transparent text-slate-500'"
            class="px-4 py-2 border-b-2 font-semibold text-sm transition-colors"
        ><?= $block->escapeHtml($tab['label']) ?></button>
        <?php endforeach; ?>
    </div>
    <?php foreach ($tabs as $index => $tab): ?>
    <div x-show="active === <?= (int) $index ?>" x-cloak class="prose prose-sm max-w-none">
        <?= /* @noEscape */ $tab['html'] ?>
    </div>
    <?php endforeach; ?>
</div>

It is important to control the active tab index through x-show and Alpine bindings rather than a mustache expression, since Page Builder templates are pre rendered on the server and there is no runtime template interpolation. For text output inside Alpine components the same rule applies as everywhere else in the theme: use x-text instead of curly braces, so no unresolved placeholder is ever visible even with JavaScript disabled. That keeps Hyvä Page Builder content types with Alpine logic both maintainable and CSP safe.

6. Understanding the master format parser and placeholder conversion

Page Builder stores formatting information such as text color, alignment or padding as an HTML encoded data-pb-style attribute. When rendering Hyvä Page Builder content types, this attribute first has to be decoded, then split into a list of key value pairs and finally mapped onto a set of allowed Tailwind classes. Skipping this step and copying style attributes straight into inline styles opens the door to CSP violations and inconsistent styling.


<?php

declare(strict_types=1);

namespace Mironsoft\PageBuilder\Model\MasterFormat;

/**
 * Decodes Page Builder master-format style attributes into whitelisted Tailwind utility classes.
 */
class StyleAttributeResolver
{
    /** @var string[] */
    private const ALLOWED_KEYS = ['text-align', 'background-color', 'padding'];

    /**
     * Convert a raw data-pb-style attribute string into Tailwind classes.
     *
     * @param string $rawStyle
     * @return string
     */
    public function toTailwindClasses(string $rawStyle): string
    {
        // Master format stores style as HTML-encoded "key: value;" pairs
        $decoded = html_entity_decode($rawStyle, ENT_QUOTES | ENT_HTML5);
        $pairs = array_filter(array_map('trim', explode(';', $decoded)));
        $classes = [];

        foreach ($pairs as $pair) {
            [$key, $value] = array_pad(explode(':', $pair, 2), 2, '');
            $key = trim($key);
            if (!in_array($key, self::ALLOWED_KEYS, true)) {
                continue;
            }
            $classes[] = $this->mapToTailwind($key, trim($value));
        }

        return implode(' ', array_filter($classes));
    }

    /**
     * Map a single CSS key/value pair to its Tailwind utility equivalent.
     *
     * @param string $key
     * @param string $value
     * @return string
     */
    private function mapToTailwind(string $key, string $value): string
    {
        return match ($key) {
            'text-align' => 'text-' . $value,
            'padding' => 'p-4',
            default => '',
        };
    }
}

Typical pitfalls show up when content is pasted from Word or Google Docs into the Page Builder editor: special characters get double encoded, and nested content types can carry encoded quotation marks that are not fully resolved by a single decode pass. For robust Hyvä Page Builder content types it is therefore recommended to run the decode step once centrally in the renderer instead of repeating it in every single template.

7. Keeping the admin editor preview and frontend rendering consistent

The Page Builder editor in the admin still uses its own Knockout based preview template, regardless of the fact that the frontend renders through Hyvä phtml. If the preview and frontend templates drift visually apart, editors lose trust in the what you see is what you get preview, and the back and forth between editorial staff and development increases.

In practice it helps to maintain shared values such as spacing, font sizes and color palettes in a central configuration and reference them from both the Knockout preview template and the phtml frontend template, instead of maintaining them twice independently. That keeps the preview for Hyvä Page Builder content types in sync with the actual rendering in the store, even after design adjustments.

A regular visual comparison between the admin preview iframe and the published page is a good test routine, ideally automated through screenshot diffs in the CI pipeline. This catches drift before editors notice it in production.

8. Performance for custom content types

Since Hyvä Page Builder content types already avoid any extra bundle JavaScript, performance optimization shifts toward images and heavier components such as sliders. Images inside a content type should generally get loading="lazy", while slider content types additionally benefit from an intersection observer that only loads image sources once they reach the viewport.


// Lazy-load slider images inside Page Builder content types only once visible
document.addEventListener('DOMContentLoaded', () => {
    const sliders = document.querySelectorAll('[data-pagebuilder-slider]');

    const observer = new IntersectionObserver((entries, obs) => {
        entries.forEach((entry) => {
            if (!entry.isIntersecting) {
                return;
            }
            const slider = entry.target;
            slider.querySelectorAll('img[data-src]').forEach((img) => {
                img.src = img.dataset.src;
                img.removeAttribute('data-src');
            });
            obs.unobserve(slider);
        });
    }, { rootMargin: '200px 0px' });

    sliders.forEach((slider) => observer.observe(slider));
});

Since Hyvä ships no RequireJS at all, the overhead of module definitions and asynchronous loading of AMD dependencies that Luma would need for every slider content type simply does not exist. In practice this approach noticeably lowers Largest Contentful Paint, because Hyvä Page Builder content types only trigger image load once visitors actually scroll.

9. Migrating existing Luma Page Builder content to Hyvä rendering

The big advantage during migration: the HTML stored in the master format itself does not change when switching from Luma to Hyvä, since it lives in the database independently of the rendering layer. Switching to Hyvä Page Builder content types only affects the rendering layer on the frontend, not the stored content itself.

Breaking changes typically show up for content types that relied on jQuery UI plugins or third party slider libraries in Luma, for which no Hyvä template exists yet. If the mapping is missing from content_type.xml, Hyvä renders an empty or generic fallback instead of the expected content, which in the worst case only surfaces during a spot check.

A solid test strategy is to systematically list every content type appearance used across CMS pages and categories before the theme switch, and check it against the available Hyvä templates. Only once every used appearance has a matching phtml template can you be confident that Hyvä Page Builder content types cover the entire existing content library.

Many teams underestimate how differently content type rendering turns out depending on the chosen approach. The overview below compares common pitfalls when building custom Hyvä Page Builder content types against the recommended pattern.

Task Unsafe / Inefficient Recommended Pattern Benefit
Frontend rendering Generic Page Builder CSS classes Custom phtml template with Tailwind classes Consistent design system, no CSS bloat
Interactivity Extra bundle JavaScript / Knockout widget Alpine.js x-data directly in the template No extra bundle, CSP compliant
Images in content type Eager loading all images loading="lazy" plus intersection observer Better LCP scores, less data volume
Admin preview Preview template drifts from the frontend Shared design tokens for preview and frontend Consistent WYSIWYG for editors
Content type registration Overriding core templates directly Custom content_type.xml with its own namespace Upgrade safe, no core conflicts
Style attributes Copying inline styles unchecked Validating style attributes and mapping to Tailwind No CSS chaos, CSP clean

Mironsoft

Hyvä frontend development and Page Builder integration for Magento 2

Need custom Page Builder content types for your Hyvä theme?

We register, render and style custom Hyvä Page Builder content types, migrate existing Luma content and ensure performant, CSP compliant rendering without any extra bundle JavaScript.

Content Type Development

Register custom content types with content_type.xml, phtml templates and Alpine.js

Page Builder Migration

Analyze existing Luma content and move it over to Hyvä rendering

Hyvä Frontend Design

Tailwind based styling and performance tuning for content building blocks

10. Summary

Custom Hyvä Page Builder content types solve a fundamental architectural problem: they replace the client side Knockout rendering chain from Luma with server side phtml rendering that needs no extra bundle JavaScript and can be styled consistently with Tailwind classes. Registration happens declaratively through content_type.xml and appearance.xml, and the actual rendering is handled by a regular phtml template with escaped field values.

Interactivity is added through Alpine.js, which is already loaded globally in Hyvä, while performance optimization mainly focuses on lazy loading images and sliders. The master format itself stays unchanged in the database when switching from Luma to Hyvä, so migration primarily affects the rendering layer, not the stored content.

Anyone who builds Hyvä Page Builder content types consistently around these patterns from the start avoids later refactoring: preview and frontend templates stay in sync, style attributes are handled deliberately instead of copied raw, and every new content type fits seamlessly into the theme's existing design system.

Hyvä Page Builder Content Types: The Essentials at a Glance

Content Type Registration

content_type.xml and appearance.xml declare the name, appearances and form fields, and the template attribute points to the phtml rendering template.

Master Format Rendering

A PHP renderer walks the structure tree on the server instead of hydrating Knockout widgets in the browser. Style attributes are decoded and mapped under control.

Alpine.js Interactivity

Tabs, accordions and similar interactions run through x-data, x-show and x-text, without loading any extra bundle JavaScript.

Performance & Migration

Lazy loading for images and sliders, shared design tokens for preview and frontend, systematic appearance matching before a theme switch.

11. FAQ: Hyvä Page Builder Content Types

1What exactly are Hyvä Page Builder content types?
Page Builder building blocks whose frontend rendering happens server side through phtml templates instead of Knockout widgets. Editor and registration stay identical to Magento_PageBuilder.
2Why does Hyvä render Page Builder differently than Luma?
Luma binds Knockout components through RequireJS. Hyvä avoids Knockout and jQuery, so a PHP renderer handles the entire translation of the master format.
3How do I register a custom content type?
content_type.xml defines the name and appearances, appearance.xml the form fields. The template attribute additionally points to the Hyvä phtml template.
4What files does a new content type need?
At minimum content_type.xml, one appearance.xml per appearance and one phtml template per appearance. Optionally a block or view model class.
5How do I add Alpine.js interactivity?
Declare x-data directly in the phtml template, control state through x-show, x-bind and x-text. Alpine is loaded globally in Hyvä, no extra bundle needed.
6What is the master format?
The HTML structure with data-content-type and data-pb-style attributes that Page Builder uses to store content. Independent of the rendering layer, stays unchanged on a theme switch.
7How do I keep preview and frontend in sync?
Reference shared design tokens for spacing, font sizes and colors in both the preview and frontend template instead of maintaining them twice.
8How do I avoid performance problems?
loading="lazy" for images, intersection observer for sliders, no extra bundle JavaScript. Hyvä ships no RequireJS anyway.
9How do I migrate Luma content to Hyvä?
The master format stays unchanged, only the rendering layer switches. List all used appearances beforehand and match them against available Hyvä templates.
10What happens without a matching Hyvä template?
A generic fallback template kicks in and displays content unformatted or incompletely. Match all appearances fully before every theme switch.