Hyvä-Produktseite erweitern: Galerie, Tabs, Attribute
AI generated
Hyvä
phtml
Hyvä · Magento 2 · Alpine.js · Tailwind CSS
Hyvä-Produktseite erweitern: Galerie, Tabs, eigene Attribute
Layout-XML statt Block-Overrides, ViewModels statt jQuery-Plugins

Wer die Produktdetailseite im Hyvä-Theme erweitern will, greift nicht zu Fotorama-Plugins oder Knockout-Templates, sondern zu Layout-XML, ViewModels und Alpine.js-Komponenten. Diese Anleitung zeigt an konkreten Codebeispielen, wie Galerie, Tabs und eigene Attribute sauber ins bestehende Hyvä-Blocksystem eingebunden werden, inklusive GraphQL-Anbindung, Performance-Optimierung und CSP-konformer Deploy-Sequenz.

18 Min. Lesezeit Layout-XML · ViewModel · Alpine.js · GraphQL Magento 2.4.8-p4 · PHP 8.4 · Tailwind v4

1. Warum PDP-Customizing in Hyvä anders funktioniert als in Luma

In Luma bestand das Anpassen der Produktdetailseite meist darin, eine Block-Klasse zu überschreiben (preference in di.xml) und ein KnockoutJS-Template mit data-bind-Direktiven zu duplizieren. Jede kleine Änderung an der Galerie oder den Tabs zog dabei ein Bündel an jQuery-Widgets, RequireJS-Modulen und UI-Components-Konfiguration nach sich. Die Hyvä-Produktseite verzichtet komplett auf dieses Konstrukt: Es gibt kein fotorama.js, kein Knockout-Binding und keine ui_component-XML, die zwischen Frontend und Backend vermittelt. Stattdessen steuert reines Layout-XML, welche Blöcke geladen werden, und phtml-Templates rendern serverseitig fertiges HTML, das Alpine.js im Browser interaktiv macht.

Der zentrale Unterschied beim Produktseiten-Customizing liegt in der Verantwortlichkeit der PHP-Klassen. Wo Luma-Blöcke oft Geschäftslogik, Rendering-Hilfsfunktionen und Zustandsverwaltung vermischen, setzt Hyvä konsequent auf ViewModels, die \Magento\Framework\View\Element\Block\ArgumentInterface implementieren. Ein ViewModel liefert ausschließlich Daten und Berechnungslogik an das Template, ohne selbst Teil der Block-Hierarchie zu sein. Das macht ViewModels leicht testbar, wiederverwendbar über mehrere Templates hinweg und unabhängig vom schwergewichtigen Block-Lifecycle. Für die Hyvä-Produktseite bedeutet das: Ein neues Attribut, eine neue Berechnung oder ein neuer Datenzugriff landet fast immer zuerst in einem ViewModel, nicht in einer neuen Block-Klasse.

Ein dritter Punkt betrifft den fehlenden UI-Components- und KnockoutJS-Overhead. Wo Luma für jede kleine Interaktion (Tab-Wechsel, Bild-Zoom, Mengenauswahl) ein eigenes RequireJS-Modul samt Konfiguration in requirejs-config.js braucht, übernimmt in Hyvä Alpine.js diese Aufgabe direkt im Template über x-data, x-show und x-on. Der JavaScript-Payload einer Hyvä-PDP bleibt dadurch minimal, es gibt kein Bundle-Splitting-Problem und keine Race-Conditions zwischen RequireJS-Modulen. Wer aus der Luma-Welt kommt, muss beim Produktseiten-Customizing also nicht nur andere Werkzeuge lernen, sondern grundsätzlich in Layout-XML und Templates statt in Block-Klassen und Knockout-Bindings denken.

2. Architektur der Produktseite: Layout-XML-Hierarchie und Container

Der Einstiegspunkt für jede Anpassung der Hyvä-Produktseite ist das Layout-Handle catalog_product_view.xml, ergänzt um typspezifische Handles wie catalog_product_view_type_simple oder catalog_product_view_type_configurable. Innerhalb dieser Hierarchie liegt der Container product.info.main, der die Kernblöcke der Produktansicht bündelt: Titel, Preis, Galerie-Referenz und Formular. Ein zweiter wichtiger Container ist product.info.details, der die Tab-Struktur (Description, Additional Information, individuelle Tabs) aufnimmt. Wer die Hyvä-Produktseite erweitern will, referenziert diese Container mit referenceContainer oder referenceBlock, statt bestehende Templates zu kopieren und global zu ersetzen.

Template-Overrides erfolgen im eigenen Theme unter app/design/frontend/Mironsoft/default/Magento_Catalog/templates/product/view/. Ein Override kopiert dabei nur die Datei, die tatsächlich verändert werden soll, etwa gallery.phtml oder additional.phtml, und lässt alle anderen Templates unverändert aus dem Parent-Theme hyva-themes/magento2-default-theme-csp erben. Dieses selektive Überschreiben ist einer der größten Vorteile gegenüber Luma: Ein Theme-Update im Parent bricht nicht automatisch alle eigenen Anpassungen, weil nur die tatsächlich geänderten Dateien im eigenen Theme-Ordner liegen.

Für neue Blöcke, etwa einen zusätzlichen Tab oder einen eigenen Info-Bereich, wird in der eigenen catalog_product_view.xml ein block-Element mit name, template und optional einem viewModel-Argument in referenceBlock ergänzt. Die Bindung des ViewModels erfolgt über ein Argument vom Typ \Magento\Framework\View\Element\Block\ArgumentInterface, das im Konstruktor des Templates über $block->getViewModel() abrufbar ist. Diese Trias aus Layout-XML, phtml-Template und ViewModel ist das durchgängige Muster für jede Erweiterung der Hyvä-Produktseite, egal ob es um Galerie, Tabs oder Attribute geht.


<!-- app/design/frontend/Mironsoft/default/Magento_Catalog/layout/catalog_product_view.xml -->
<!-- English comment: extend the product info details container with a custom tab block -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="product.info.details">
            <block class="Magento\Framework\View\Element\Template"
                   name="product.info.warranty"
                   template="Mironsoft_ProductPage::product/view/warranty.phtml"
                   before="-">
                <arguments>
                    <argument name="viewModel" xsi:type="object">Mironsoft\ProductPage\ViewModel\WarrantyInfo</argument>
                </arguments>
            </block>
        </referenceContainer>
    </body>
</page>

3. Galerie erweitern: Alpine.js statt Fotorama

Hyvä ersetzt Fotorama vollständig durch eine schlanke Alpine.js-Komponente, die im Standardtheme unter gallery.phtml liegt. Für eine erweiterte Hyvä-Produktseite mit Thumbnails, Zoom und Video-Unterstützung überschreibt man dieses Template im eigenen Theme und erweitert die x-data-Funktion um zusätzlichen Zustand: einen Index für das aktive Bild, ein Flag für den Zoom-Modus und ein Feld für die aktuell abgespielte Video-URL. Die Bilddaten selbst kommen weiterhin aus dem Standard-ViewModel Magento\Catalog\ViewModel\Product\Gallery\GalleryImages, das bereits vollständig aufbereitete Bild-Arrays inklusive Video-Metadaten liefert.

Der Zoom wird typischerweise über x-on:mousemove auf dem Hauptbild realisiert, das per background-position die Cursor-Position auf ein vergrößertes Hintergrundbild abbildet. Für Video-Unterstützung prüft die Komponente pro Galerie-Item, ob ein videoUrl-Feld vorhanden ist, und rendert stattdessen ein iframe oder ein natives video-Element anstelle des img-Tags. Wichtig für die Hyvä-Produktseite: Die gesamte Zustandslogik bleibt im Alpine-Scope des Templates, es gibt keinen zusätzlichen globalen JavaScript-Store und keine Abhängigkeit zu einer externen Lightbox-Bibliothek.

Die Anbindung an eigene Daten, etwa ein zusätzliches 360-Grad-Bildset, erfolgt über ein eigenes ViewModel, das die Standard-Galeriedaten um die zusätzlichen Einträge ergänzt, statt das Kern-ViewModel zu überschreiben. Diese Kombination aus Composition und Alpine-State-Erweiterung erlaubt es, die Galerie der Hyvä-Produktseite Schritt für Schritt zu erweitern, ohne die Basis-Templates des Parent-Themes zu duplizieren.


// app/design/frontend/Mironsoft/default/Magento_Catalog/web/js/product-gallery.js
// English comment: Alpine.js gallery component with thumbnails, zoom and video support
export default function productGallery(images) {
  return {
    images: images,
    activeIndex: 0,
    zoomActive: false,
    zoomStyle: '',

    get activeImage() {
      return this.images[this.activeIndex];
    },

    selectImage(index) {
      this.activeIndex = index;
      this.zoomActive = false;
    },

    onZoomMove(event) {
      const rect = event.currentTarget.getBoundingClientRect();
      const x = ((event.clientX - rect.left) / rect.width) * 100;
      const y = ((event.clientY - rect.top) / rect.height) * 100;
      this.zoomStyle = `background-position: ${x}% ${y}%;`;
      this.zoomActive = true;
    },

    isVideo(item) {
      return Boolean(item.videoUrl);
    }
  };
}

4. Tabs anpassen und einen eigenen Tab hinzufügen

Die Standard-Tabs der Hyvä-Produktseite, Beschreibung und zusätzliche Informationen, werden über den Container product.info.details gerendert und teilen sich eine gemeinsame Alpine-Komponente, die den aktiven Tab per x-data="{ activeTab: 'description' }" im umschließenden Wrapper hält. Jeder Tab-Button setzt activeTab per x-on:click und jedes Tab-Panel prüft mit x-show="activeTab === 'description'", ob es sichtbar sein soll. Dieses Muster skaliert direkt auf zusätzliche Tabs, weil neue Panels lediglich einen weiteren Vergleichswert im selben x-show-Ausdruck benötigen.

Ein komplett neuer Tab, etwa für Garantieinformationen oder technische Datenblätter, wird über addBlock beziehungsweise ein neues block-Element im Layout-XML in den Container product.info.details eingehängt. Das zugehörige phtml-Template rendert nur den Button und das Panel-Markup, ohne eine eigene, isolierte Alpine-Instanz zu starten. Entscheidend ist, dass der neue Tab-Button und das neue Panel im selben Eltern-Scope liegen wie die bestehenden Tabs, damit sie dieselbe activeTab-Variable teilen. Wird der neue Block in einem separaten Template mit eigenem x-data gerendert, entkoppelt sich sein Zustand vom Rest der Tab-Leiste, und ein Klick auf den neuen Tab schließt die anderen Panels nicht mehr korrekt.

Für die Barrierefreiheit sollte jeder neue Tab-Button ein role="tab", eine aria-selected-Bindung an activeTab und eine eindeutige id erhalten, die im zugehörigen Panel per aria-labelledby referenziert wird. Diese ARIA-Struktur wird in Abschnitt 8 im Detail behandelt, ist aber bereits beim ersten Anlegen des neuen Tabs mitzudenken, weil ein nachträgliches Ergänzen von ARIA-Attributen in bereits produktiv laufenden Templates leicht vergessen wird.


<!-- app/design/frontend/Mironsoft/default/Magento_Catalog/templates/product/view/warranty.phtml -->
<!-- English comment: additional tab that shares the parent activeTab state -->
<?php /** @var \Magento\Framework\Escaper $escaper */ ?>
<?php $viewModel = $block->getViewModel(); ?>
<div class="border-b border-slate-200" x-show="activeTab === 'warranty'" role="tabpanel" id="panel-warranty" aria-labelledby="tab-warranty">
    <div class="py-6 text-sm text-gray-700">
        <?= $escaper->escapeHtml($viewModel->getWarrantyText()) ?>
    </div>
</div>

5. Eigene Attribute anzeigen: EAV, ViewModel und Escaping

Ein eigenes Attribut für die Hyvä-Produktseite beginnt mit einem deklarativen Setup-Patch, der \Magento\Eav\Setup\EavSetup nutzt, um das Attribut am Produkt-Entity anzulegen. Wichtig ist, das Attribut mit used_in_product_listing und einem sinnvollen frontend_input zu versehen, damit es sowohl in der Produktübersicht als auch im Katalog performant abrufbar ist. Nach dem Anlegen wird das Attribut nicht direkt im Template über $product->getData('attribute_code') ausgelesen, sondern über eine dedizierte Methode im ViewModel gekapselt.

Diese Kapselung im ViewModel hat einen konkreten Grund: Fallback-Logik, Formatierung und Null-Prüfungen gehören in PHP-Code, der isoliert getestet werden kann, nicht ins Template. Die ViewModel-Methode nimmt das aktuelle Produkt entgegen, liest den Attributwert aus, wendet bei Bedarf eine Source-Model-Auflösung an (für Select- oder Multiselect-Attribute) und gibt einen fertig aufbereiteten String zurück. Das phtml-Template ruft ausschließlich diese Methode auf und escaped die Ausgabe konsequent mit $escaper->escapeHtml(), selbst wenn der Wert aus einem vermeintlich vertrauenswürdigen Backend-Feld stammt.

Für die Hyvä-Produktseite gilt dabei dieselbe Regel wie für jedes andere Hyvä-Template: Niemals direkten PHP-Output ohne Escaper-Aufruf, auch nicht bei numerischen oder scheinbar sicheren Werten, weil Attributwerte über den Admin-Bereich frei editierbar sind und damit als potenziell unsichere Eingabe behandelt werden müssen. Bei HTML-Attributen mit erlaubter Formatierung kommt stattdessen escapeHtml() mit einer expliziten Allowlist an Tags zum Einsatz, niemals ein ungeprüftes echo.


<?php
declare(strict_types=1);

namespace Mironsoft\ProductPage\ViewModel;

use Magento\Catalog\Model\Product;
use Magento\Framework\View\Element\Block\ArgumentInterface;

/**
 * ViewModel that exposes the custom warranty attribute to product page templates.
 */
final class WarrantyInfo implements ArgumentInterface
{
    private const ATTRIBUTE_CODE = 'warranty_period';

    /**
     * @param Product $product Current product model injected from the block scope.
     */
    public function __construct(
        private readonly Product $product
    ) {
    }

    /**
     * Return a formatted warranty period for the current product, or an empty string.
     *
     * @return string
     */
    public function getWarrantyText(): string
    {
        $value = $this->product->getData(self::ATTRIBUTE_CODE);

        if ($value === null || $value === '') {
            return '';
        }

        return sprintf('%d Monate Herstellergarantie', (int) $value);
    }
}

6. GraphQL-Anpassung: eigenes Attribut im products-Query

Damit das neue Attribut auch im Headless- oder PWA-Kontext verfügbar ist, muss es explizit im GraphQL-Schema exponiert werden. Magento erweitert Schemata additiv über schema.graphqls-Dateien mit dem Schlüsselwort extend type ProductInterface. Ein rein deklaratives extend reicht aus, wenn das Feld direkt aus einem EAV-Attribut mit gleichem Namen auflösbar ist, in vielen Fällen braucht die Hyvä-Produktseite im GraphQL-Kontext aber zusätzliche Formatierung, etwa die gleiche Textaufbereitung wie im ViewModel.

Für formatierte Ausgaben implementiert man \Magento\Framework\GraphQl\Query\ResolverInterface in einer eigenen Resolver-Klasse und bindet sie über das @resolver-Directive im Schema an das neue Feld. Der Resolver erhält das aufgelöste Produktmodell über $value['model'] im resolve()-Aufruf und kann dieselbe Formatierungslogik nutzen, die auch im PHP-ViewModel für die serverseitig gerenderte Hyvä-Produktseite zum Einsatz kommt, idealerweise über einen gemeinsam genutzten Formatter statt Code-Duplikation.

Ein Punkt, der in der Praxis häufig übersehen wird: GraphQL-Caching arbeitet mit eigenen Cache-Tags, die unabhängig vom Block-HTML-Cache sind. Wird das neue Attribut im Admin geändert, muss sichergestellt sein, dass der zugehörige Produkt-Cache-Tag auch für GraphQL-Antworten invalidiert wird, sonst zeigt die REST- oder Blockversion der Hyvä-Produktseite einen anderen Attributwert als die GraphQL-gestützte Variante.


# app/code/Mironsoft/ProductPage/etc/schema.graphqls
# English comment: expose the custom warranty attribute on ProductInterface
extend type ProductInterface {
    warranty_text: String @resolver(class: "Mironsoft\\ProductPage\\Model\\Resolver\\WarrantyText")
        @doc(description: "Formatted warranty period text for the product page")
}

7. Performance: Lazy Loading und Layout-Shifts vermeiden

Die Galerie ist auf der Hyvä-Produktseite meist das größte Performance-Risiko, weil mehrere hochauflösende Bilder gleichzeitig angefordert werden können. Nur das aktive Hauptbild sollte eager geladen werden (loading="eager" plus fetchpriority="high"), während alle Thumbnails und nicht sichtbaren Galerie-Bilder loading="lazy" erhalten. Entscheidend für Core Web Vitals ist zusätzlich, jedem img-Tag explizite width- und height-Attribute oder ein CSS-Aspect-Ratio mitzugeben, damit der Browser den benötigten Platz vor dem eigentlichen Laden reserviert und kein Cumulative Layout Shift entsteht, wenn das Bild nachträglich einblendet.

Für neue Templates, etwa den zusätzlichen Warranty-Tab oder eine erweiterte Galerie-Komponente, muss die Tailwind-v4-Konfiguration wissen, dass in diesen Dateien Utility-Klassen verwendet werden. Da Tailwind v4 CSS-first arbeitet, erfolgt das über die @source-Direktive im Haupt-CSS-Entrypoint, die zusätzliche Template-Pfade außerhalb des Standard-Scans einschließt. Fehlt dieser Eintrag, werden Utility-Klassen im neuen Template zwar im HTML ausgegeben, aber nicht ins finale CSS-Bundle aufgenommen, was zu unstyled, aber funktional korrektem Markup führt, ein Fehlerbild, das in Reviews oft fälschlich als PHP-Bug interpretiert wird.

Ein dritter Performance-Hebel betrifft die Alpine-Initialisierung selbst: Wird die Galerie-Komponente mit großen, bereits vollständig aufgelösten Bild-Arrays initialisiert, entsteht ein spürbarer Parsing-Overhead beim ersten Rendern. Für Produkte mit sehr vielen Bildern (zum Beispiel Konfigurationsbeispiele oder 360-Grad-Sets) lohnt es sich, nur die ersten Bilder inline zu übergeben und weitere Bilder erst bei Bedarf per Fetch nachzuladen, statt die komplette Galerie beim initialen Seitenaufbau in den DOM zu schreiben.

8. Barrierefreiheit: ARIA-Labels und Keyboard-Navigation

Für die Tab-Struktur der Hyvä-Produktseite gilt das WAI-ARIA-Authoring-Practices-Muster: Der umschließende Container der Tab-Buttons erhält role="tablist", jeder Button role="tab" mit aria-selected gebunden an den Alpine-State, und jedes Panel role="tabpanel" mit aria-labelledby, das auf die zugehörige Tab-Button-ID verweist. Ohne diese Rollen liest ein Screenreader die Tabs als beliebige Buttons vor, ohne die Zusammengehörigkeit von Button und Panel zu kommunizieren, was die Navigation für assistive Technologien erheblich erschwert.

Keyboard-Navigation ergänzt die ARIA-Rollen um tatsächliche Bedienbarkeit ohne Maus. Pfeiltasten links und rechts sollten zwischen den Tabs wechseln, ohne dass der Fokus das Element verlässt, während Enter und Leertaste den fokussierten Tab aktivieren. In Alpine.js lässt sich das über x-on:keydown.right und x-on:keydown.left direkt auf dem Tab-Button umsetzen, kombiniert mit x-ref, um den Fokus programmatisch auf den nächsten Button zu setzen. Für die Galerie gilt Analoges: Thumbnails müssen per Tab-Taste erreichbar sein, und das aktive Hauptbild sollte bei Bildwechsel nicht den Fokus stehlen, damit Tastaturnutzer nicht bei jedem Klick zurück an den Seitenanfang springen.

Alt-Texte für Galeriebilder verdienen bei der Hyvä-Produktseite besondere Aufmerksamkeit, weil das Standard-ViewModel oft nur den generischen Produktnamen als Alt-Text liefert. Für eine bessere Zugänglichkeit lohnt es sich, bildspezifische Alt-Texte im Produktbild-Attribut zu pflegen und diese im ViewModel gegenüber dem generischen Fallback zu bevorzugen, damit Screenreader-Nutzer tatsächlich unterscheiden können, welches Bild gerade aktiv ist.

9. Deployment und Testing: CSP-konforme Scripts, Deploy-Sequenz

Hyvä läuft standardmäßig mit einer strikten Content-Security-Policy, die Inline-Scripts ohne explizite Registrierung blockiert. Jeder <script>-Inline-Block in einem Template der Hyvä-Produktseite, etwa zur Übergabe von Alpine-Initialdaten als JSON, muss deshalb direkt im Anschluss mit $hyvaCsp->registerInlineScript() registriert werden. Ohne diese Registrierung blockiert der Browser das Skript im Produktivbetrieb lautlos, während es in der lokalen Entwicklungsumgebung mit deaktivierter CSP unauffällig funktioniert, ein klassischer Fehler, der erst nach dem Deployment auf einer Staging- oder Live-Umgebung sichtbar wird.

Nach jeder Änderung an Templates, ViewModels oder der Tailwind-Konfiguration ist die Deploy-Sequenz strikt einzuhalten. Zuerst wird das CSS für das betroffene Theme neu gebaut, danach werden die vorverarbeiteten View-Dateien und statischen Assets vollständig gelöscht, bevor setup:static-content:deploy erneut läuft. Wird dieser Löschschritt übersprungen, mischen sich unter Umständen alte und neue Tailwind-Klassen im ausgelieferten CSS, was zu inkonsistentem Styling führt, das nur schwer zu reproduzieren ist.


{
  "attribute_code": "warranty_period",
  "frontend_input": "text",
  "value": "24",
  "resolved_label": "24 Monate Herstellergarantie"
}

Für das eigentliche Testing der neuen Features empfiehlt sich eine Kombination aus manuellem Klicktest mit aktivierter CSP im Staging und automatisierten Tests für den ViewModel selbst, etwa ein einfacher PHPUnit-Test, der prüft, dass getWarrantyText() bei fehlendem Attributwert einen leeren String statt eines Fehlers zurückgibt. Diese Kombination aus manueller UI-Prüfung und automatisiertem Unit-Test für die PHP-Logik deckt die zwei Fehlerklassen ab, die bei der Erweiterung der Hyvä-Produktseite am häufigsten auftreten: CSP-Verstöße im Frontend und Null-Pointer-artige Fehler im Backend.

Vergleich: Naiver Ansatz vs. empfohlenes Hyvä-Pattern

Aufgabe Naiver / Luma-artiger Ansatz Empfohlenes Hyvä-Pattern Vorteil
Galerie erweitern Fotorama-Plugin patchen Alpine.js x-data Komponente Kein jQuery, minimaler JS-Payload
Neuen Tab einbauen Block-Klasse überschreiben referenceContainer + ViewModel Update-sicher, testbar
Attribut ausgeben echo $product->getData(...) $escaper->escapeHtml($viewModel->...) Kein XSS-Risiko
Template überschreiben Ganzes Theme kopieren Selektive Template-Overrides Parent-Theme-Updates bleiben nutzbar
Inline-Script nutzen Ungeregistriertes <script> $hyvaCsp->registerInlineScript() Funktioniert mit strikter CSP

Mironsoft

Hyvä-Theme-Entwicklung und Magento-2-Agentur

Eure Hyvä-Produktseite soll mehr können?

Wir erweitern Galerien, Tabs und Attribute im Hyvä-Theme sauber über Layout-XML und ViewModels, inklusive GraphQL-Anbindung, Performance-Optimierung und CSP-konformen Alpine.js-Komponenten.

PDP-Audit

Analyse bestehender Produktseiten-Templates auf Performance und Barrierefreiheit

Galerie & Tabs

Eigene Alpine.js-Komponenten für Zoom, Video und zusätzliche Tabs

GraphQL-Resolver

Eigene Attribute im products-Query für Headless-Frontends exponieren

10. Zusammenfassung

Das Erweitern der Hyvä-Produktseite folgt immer demselben Grundmuster: Layout-XML bestimmt, welche Blöcke wo erscheinen, ViewModels liefern Daten und Logik, und phtml-Templates rendern serverseitiges HTML, das Alpine.js im Browser interaktiv macht. Die Galerie ersetzt Fotorama durch eine schlanke x-data-Komponente, neue Tabs teilen sich den activeTab-State mit den Standard-Tabs, und eigene Attribute werden über EAV-Setup, ViewModel-Kapselung und konsequentes Escaping ausgegeben.

Für Headless-Szenarien reicht die Anpassung nicht bei PHP-Templates: GraphQL-Schema-Erweiterungen und Resolver sorgen dafür, dass dieselben Daten auch außerhalb des serverseitigen Renderings verfügbar sind. Performance und Barrierefreiheit sind dabei keine nachgelagerten Optimierungsschritte, sondern Teil der Grundarchitektur, Lazy Loading, ARIA-Rollen und CSP-konforme Inline-Scripts gehören von Anfang an ins Template, nicht als späterer Fix. Wer die Deploy-Sequenz aus Rebuild, Cache-Löschen und Static-Content-Deploy konsequent einhält, vermeidet die häufigsten Inkonsistenzen zwischen lokalem Test und Live-Stand.

Hyvä-Produktseite erweitern: Das Wichtigste auf einen Blick

Architektur

Layout-XML mit referenceContainer und referenceBlock statt Block-Klassen-Overrides, ViewModels über ArgumentInterface.

Galerie & Tabs

Alpine.js x-data statt Fotorama, geteilter activeTab-State für neue und bestehende Tabs.

Attribute & GraphQL

EAV-Setup, ViewModel-Kapselung mit $escaper, GraphQL-Erweiterung via extend type ProductInterface.

Deployment

$hyvaCsp->registerInlineScript() nach jedem Inline-Script, strikte Deploy-Sequenz mit Rebuild und Cache-Flush.

11. FAQ: Hyvä-Produktseite erweitern

1Was unterscheidet die Hyvä-Produktseite grundlegend von Luma?
Kein KnockoutJS, keine UI-Components, kein jQuery. Layout-XML steuert Blöcke, ViewModels liefern Daten, Alpine.js übernimmt Interaktivität im Template.
2Wo lege ich Template-Overrides für die Produktseite an?
Unter app/design/frontend/Mironsoft/default/Magento_Catalog/templates/product/view/. Nur veränderte Dateien werden kopiert, der Rest erbt vom Parent-Theme.
3Warum ViewModels statt Block-Klassen für neue Attribute?
ViewModels implementieren ArgumentInterface, sind testbar und unabhängig vom Block-Lifecycle. Sie kapseln Formatierung sauber getrennt vom Template.
4Wie ersetze ich Fotorama in der Hyvä-Produktseite?
Hyvä nutzt bereits eine Alpine.js-Galerie. Zoom oder Video ergänzt man im x-data-Objekt, ohne Fotorama oder Lightbox-Libraries einzubinden.
5Wie füge ich einen komplett neuen Tab hinzu?
Neues Block-Element im Layout-XML in product.info.details. Button und Panel müssen im selben Alpine-Scope wie bestehende Tabs liegen.
6Wie zeige ich ein eigenes EAV-Attribut sicher an?
Setup-Patch anlegen, Wert über ViewModel-Methode auslesen, im Template ausschließlich mit $escaper->escapeHtml() ausgeben.
7Wie exponiere ich ein eigenes Attribut in GraphQL?
extend type ProductInterface in schema.graphqls. Für formatierte Werte zusätzlich ResolverInterface implementieren und per @resolver einbinden.
8Wie vermeide ich Layout-Shifts in der erweiterten Galerie?
Explizite width/height oder feste Aspect-Ratio je img-Tag. Nur das aktive Bild eager laden, den Rest mit loading="lazy".
9Welche ARIA-Rollen brauchen die Tabs der Produktseite?
role="tablist" auf dem Container, role="tab" mit aria-selected je Button, role="tabpanel" mit aria-labelledby je Panel, plus Pfeiltasten-Navigation.
10Was passiert, wenn ich registerInlineScript() vergesse?
Der Browser blockiert das Script unter aktiver CSP lautlos. Lokal ohne CSP fällt das oft nicht auf und zeigt sich erst auf Staging oder Live.