Produktvergleich im Hyvä-Theme: Compare-Seite mit Alpine.js und GraphQL
AI generated
Hyvä
phtml
Hyvä Theme · Produktvergleich
Produktvergleich im Hyvä-Theme
Compare-Seite mit Alpine.js und der nativen GraphQL-Compare-API

Hyvä liefert Minicart und Wishlist mit, eine fertige Compare-Funktionalität sucht man im Standard-Theme jedoch vergeblich. Wer trotzdem eine Vergleichsseite braucht, sollte nicht bei null anfangen, sondern auf Magentos native Compare-Product-API setzen und sie mit einem sauberen Alpine-Store und einer responsiven Tabelle verbinden.

13 Min. Lesezeit Compare List API Alpine Store localStorage GraphQL uid Responsive Tabelle

1. Warum Hyvä keine fertige Compare-Lösung mitbringt

Hyvä Themes reduziert das Standard-Frontend bewusst auf das, was die meisten Shops tatsächlich brauchen. Minicart, Wishlist und Checkout gehören dazu, eine Compare-Seite gehört bei den meisten Projekten nicht zum Kern des Sortiments-Erlebnisses und fehlt deshalb im Standard-Theme. Das ist keine technische Lücke, sondern eine bewusste Reduktion, die verhindert, dass Shops ungenutzte Komplexität mit ausliefern.

Für Sortimente mit vielen vergleichbaren Varianten, etwa Elektronik, Werkzeuge oder technische Komponenten, ist ein Produktvergleich trotzdem oft ein echtes Conversion-Feature. Statt eine komplett eigene Datenhaltung zu bauen, lohnt sich der Blick auf die Compare-Product-API, die Magento seit Jahren mitbringt und die seit einigen Versionen auch vollständig über GraphQL ansprechbar ist. Damit lässt sich die Funktionalität im Hyvä-typischen Stil nachrüsten, ohne die Legacy-Blöcke aus dem Luma-Theme zu reaktivieren.

2. Die native Compare-Product-API statt Eigenbau

Magento verwaltet Vergleichslisten serverseitig über eine eigene uid, ganz analog zur maskierten Cart-Id einer Gastbestellung. Die Mutation createCompareList erzeugt eine neue Liste und liefert die uid zurück, addProductsToCompareList und removeProductsFromCompareList pflegen den Inhalt, und die Query compareList liefert alle Produkte samt Attributen für die Vergleichstabelle. Diese uid wird clientseitig gespeichert und bei jeder weiteren Anfrage mitgeschickt, genau wie die Cart-Id im Checkout.

Der Vorteil gegenüber einer Eigenlösung mit eigenem Custom-Attribut oder eigener Datenbanktabelle ist offensichtlich: Die Compare-Product-API kennt bereits Konfigurierbarkeit, Preisregeln, Store-Views und Berechtigungen, sie wird mit jedem Magento-Core-Update mitgepflegt, und sie funktioniert identisch für Gäste und eingeloggte Kunden. Eigenbau lohnt sich hier fast nie, das Rad wurde bereits erfunden und liegt nur eine GraphQL-Query entfernt.


mutation CreateCompareList {
  createCompareList(input: { uid: null }) {
    uid
    item_count
    items {
      uid
      product {
        sku
        name
      }
    }
  }
}

mutation AddToCompare($listUid: ID!, $productUid: ID!) {
  addProductsToCompareList(
    input: { uid: $listUid, products: [$productUid] }
  ) {
    uid
    item_count
  }
}

3. Alpine-Store für die Vergleichsliste über mehrere Seiten hinweg

Damit die Vergleichsliste beim Wechsel von Kategorieseite zu Kategorieseite und nach einem harten Seiten-Reload erhalten bleibt, reicht ein x-data auf Komponentenebene nicht aus. Die uid muss global verfügbar sein, deshalb bietet sich ein Alpine.store an, der beim Start der Seite aus localStorage gelesen und bei jeder Änderung dorthin zurückgeschrieben wird. Hyvä bringt Alpine ohnehin global ein, ein zusätzliches Plugin wird dafür nicht gebraucht.

Wichtig ist, den Store einmalig zu initialisieren, bevor irgendeine Komponente ihn liest, sonst kommt es zu einem kurzen Flackern des Compare-Zählers beim ersten Render. In der Praxis registriert man den Store deshalb in einem eigenen Inline-Script direkt vor dem schließenden body-Tag, ähnlich wie es Hyvä für den bestehenden Wishlist-Store bereits vormacht.


document.addEventListener('alpine:init', () => {
  Alpine.store('compare', {
    uid: localStorage.getItem('compare_list_uid') || null,
    items: JSON.parse(localStorage.getItem('compare_items') || '[]'),
    count: 0,

    init() {
      this.count = this.items.length;
    },

    async addItem(productUid, listMutation) {
      const result = await listMutation(this.uid, productUid);
      this.uid = result.uid;
      this.count = result.item_count;
      localStorage.setItem('compare_list_uid', this.uid);
    },

    isInList(productUid) {
      return this.items.some((item) => item.productUid === productUid);
    },
  });
});

4. Compare-Widget im Produktgrid einbinden

Das Compare-Widget wird als kleiner Button in der bestehenden Produktkarte platziert, üblicherweise im gleichen Override von Magento_Catalog::product/list.phtml, in dem auch der Wishlist-Button sitzt. Statt eines neuen Blocks reicht ein reines Alpine-Fragment, das direkt auf den globalen Store zugreift und den Produkt-uid als Datenattribut aus dem Server-Rendering übernimmt.

Entscheidend ist die visuelle Rückmeldung: Ein Produkt, das bereits in der Vergleichsliste liegt, muss sich optisch vom Rest der Karte abheben, sonst klicken Kunden mehrfach auf denselben Button und wundern sich über Fehler beim erneuten Hinzufügen. Ein einfacher x-bind:class-Ausdruck auf Basis von isInList reicht dafür bereits aus.


<div class="product-item" x-data="{ productUid: '{{ $productUid }}' }">
  <button
    type="button"
    class="flex items-center gap-1 text-sm"
    x-bind:class="$store.compare.isInList(productUid)
      ? 'text-orange-600 font-semibold'
      : 'text-gray-500 hover:text-gray-700'"
    x-on:click="$store.compare.isInList(productUid)
      ? removeFromCompare(productUid)
      : $store.compare.addItem(productUid, addToCompareList)"
  >
    <svg class="w-4 h-4" aria-hidden="true"><!-- Icon --></svg>
    <span x-text="$store.compare.isInList(productUid) ? 'Im Vergleich' : 'Vergleichen'"></span>
  </button>
</div>

5. Guest-uid und Merge beim Login

Gäste erhalten ihre Compare-uid beim ersten Hinzufügen eines Produkts und behalten sie über localStorage, bis der Browser-Speicher geleert wird. Meldet sich ein Kunde während einer laufenden Session an, muss die Gast-Liste mit einer eventuell bereits vorhandenen Kundenliste zusammengeführt werden, sonst gehen die zuvor markierten Produkte beim Login verloren. Magento bietet dafür die Mutation assignCompareListToCustomer an, die im Login-Flow nach erfolgreicher Authentifizierung aufgerufen wird.

Praktisch heißt das: Der Login-Handler im Hyvä-Checkout beziehungsweise im Customer-Modul muss um einen zusätzlichen GraphQL-Call ergänzt werden, der die im Store gespeicherte Gast-uid übergibt. Nach erfolgreichem Merge wird die localStorage-uid gelöscht, weil die Vergleichsliste ab diesem Zeitpunkt an die Kundensitzung gebunden ist und nicht mehr clientseitig verwaltet werden muss.

6. Die Vergleichstabelle responsive gestalten

Eine Vergleichstabelle mit vier oder fünf Produkten und einem Dutzend Attributen sprengt auf Mobilgeräten fast jedes Layout. Statt die Tabelle zu verkleinern, bis nichts mehr lesbar ist, funktioniert horizontales Scrollen mit einer fixierten ersten Spalte deutlich zuverlässiger: Die Attributnamen bleiben links stehen, während die Produktspalten seitlich durchgescrollt werden können.

Mit Tailwind lässt sich das ohne zusätzliches JavaScript umsetzen, position sticky auf der ersten Spalte kombiniert mit overflow-x-auto auf dem umschließenden Container reicht bereits aus. Auf sehr kleinen Displays kann zusätzlich ein Alpine-gesteuertes Akkordeon je Produkt sinnvoller sein als eine Tabelle, gerade wenn mehr als drei Produkte verglichen werden.


<div class="overflow-x-auto">
  <table class="min-w-full border-collapse text-sm">
    <thead>
      <tr>
        <th class="sticky left-0 bg-white z-10 p-3 text-left w-40">Attribut</th>
        <template x-for="product in $store.compare.items" :key="product.uid">
          <th class="p-3 min-w-[180px] text-left" x-text="product.name"></th>
        </template>
      </tr>
    </thead>
    <tbody>
      <template x-for="attr in attributes" :key="attr.code">
        <tr class="border-t">
          <td class="sticky left-0 bg-white z-10 p-3 font-medium" x-text="attr.label"></td>
          <template x-for="product in $store.compare.items" :key="product.uid">
            <td class="p-3" x-text="product.attributes[attr.code]"></td>
          </template>
        </tr>
      </template>
    </tbody>
  </table>
</div>

7. Unterschiede zwischen Produkten optisch hervorheben

Der eigentliche Mehrwert einer Compare-Seite entsteht erst, wenn Unterschiede zwischen den Produkten sofort ins Auge fallen, statt dass Kunden jede Zeile einzeln durchlesen müssen. Dafür wird pro Attributzeile geprüft, ob alle Werte identisch sind, und bei Abweichung eine dezente Hervorhebung gesetzt, etwa ein leicht eingefärbter Hintergrund für die Zellen, die vom Mehrheitswert abweichen.

Diese Logik lässt sich rein clientseitig in Alpine berechnen, sobald die Produktdaten aus der GraphQL-Antwort im Store liegen, ganz ohne serverseitige Zusatzverarbeitung. Wichtig ist dabei, numerische Attribute wie Gewicht oder Akkulaufzeit nicht nur auf reine Textgleichheit zu prüfen, sondern normalisiert zu vergleichen, sonst werden 2.0 kg und 2 kg fälschlich als unterschiedlich markiert.

8. Performance und Full-Page-Cache-Kompatibilität

Da die gesamte Compare-Logik über GraphQL-Mutationen läuft, die naturgemäß nicht über den Full Page Cache ausgeliefert werden, bleibt die eigentliche Produktseite von der Vergleichsfunktion unberührt und weiterhin vollständig cachefähig. Der Compare-Zähler im Header wird rein clientseitig aus dem Alpine-Store gerendert, ähnlich wie die Warenkorb-Anzahl in der Minicart, wodurch keine ESI-Blöcke oder zusätzliche Cache-Ausnahmen nötig sind.

Bei der GraphQL-Query für die Vergleichstabelle selbst lohnt es sich, nur die tatsächlich angezeigten Attribute abzufragen, statt pauschal alle Produktfelder zu laden. Eine zu breite Query erhöht die Antwortzeit unnötig und überträgt Daten, die auf der Vergleichsseite ohnehin nicht dargestellt werden, besonders bei Produkten mit vielen konfigurierbaren Attributen macht sich das deutlich bemerkbar.

9. Typische Fehler beim Compare-Feature in Hyvä

Der häufigste Fehler ist, die Compare-uid nur im Alpine-Store und nicht zusätzlich in localStorage zu halten. Nach einem harten Reload ist der Store leer, die Serverliste existiert aber weiterhin, sodass Kunden scheinbar eine leere Vergleichsliste sehen, obwohl der Inhalt serverseitig noch vorhanden ist. Eine konsequente Synchronisation zwischen Store und localStorage in beide Richtungen vermeidet dieses Problem zuverlässig.

Ein zweiter, oft übersehener Punkt ist das fehlende Merge beim Login: Wird assignCompareListToCustomer vergessen, verliert der Kunde beim Anmelden alle zuvor als Gast markierten Produkte, ohne dass eine Fehlermeldung erscheint, was aus Kundensicht wie ein stiller Datenverlust wirkt. Ebenso wichtig ist die CSP-Registrierung jedes Inline-Scripts über hyvaCsp registerInlineScript, sonst blockiert die Content Security Policy den Store und die Compare-Buttons reagieren gar nicht.

Baustein Zuständigkeit Technologie Persistenz
Compare-uid Eindeutige Kennung der Vergleichsliste GraphQL createCompareList localStorage (Gast), Kundenkonto (eingeloggt)
Compare-Store Globaler Zustand über alle Seiten hinweg Alpine.store In-Memory + localStorage-Sync
Compare-Widget Hinzufügen/Entfernen im Produktgrid phtml + Alpine x-data keine, liest aus Store
Vergleichstabelle Darstellung und Attribut-Diffing GraphQL compareList Query keine, rein clientseitig gerendert
Login-Merge Zusammenführen von Gast- und Kundenliste assignCompareListToCustomer serverseitig ab Login persistent

Mironsoft

Hyvä-Theme-Entwicklung und Luma-Migration

Noch auf Luma unterwegs oder ein Hyvä-Theme, das nicht rund läuft?

Wir entwickeln Hyvä-Themes für Magento von Grund auf oder migrieren bestehende Luma-Shops sauber, mit Tailwind CSS, Alpine.js und ohne unnötiges JavaScript-Gepäck.

Luma-zu-Hyvä-Migration

Bestehenden Shop strukturiert und ohne Funktionsverlust auf Hyvä umstellen.

Custom-Theme-Entwicklung

Individuelles Hyvä-Theme nach Design-Vorgaben von Grund auf umsetzen.

Performance-Optimierung

Core Web Vitals und Ladezeiten im Hyvä-Frontend gezielt verbessern.

10. Zusammenfassung

Produktvergleich in Hyvä: Das Wichtigste auf einen Blick

Keine Eigenlösung

Magentos Compare-Product-API über GraphQL nutzen statt eigener Datenhaltung zu bauen.

Alpine-Store

Globaler Store mit localStorage-Sync hält die Vergleichsliste über Seitenwechsel hinweg.

Responsive Tabelle

Sticky erste Spalte plus horizontales Scrollen statt kaum lesbarer Mini-Tabelle.

Login-Merge

assignCompareListToCustomer verhindert Datenverlust beim Wechsel von Gast zu Kunde.

11. FAQ: Produktvergleich in Hyvä: Das Wichtigste auf einen Blick

1Bringt Hyvä eine fertige Compare-Funktionalität mit?
Nein, im Gegensatz zu Minicart und Wishlist gehört eine Compare-Seite nicht zum Standard-Umfang von Hyvä Themes. Sie muss auf Basis der nativen Magento Compare-Product-API selbst ergänzt werden, was in der Praxis überschaubarer Aufwand ist.
2Warum sollte ich nicht einfach eine eigene Compare-Tabelle in der Datenbank bauen?
Weil die native Compare-Product-API bereits Konfigurierbarkeit, Preisregeln, Store-Views und Gast-zu-Kunde-Merge korrekt abbildet und mit jedem Core-Update aktuell gehalten wird. Eine Eigenlösung müsste all das nachbauen und dauerhaft pflegen.
3Wie unterscheidet sich die Compare-uid von der Cart-Quote-Id?
Beide funktionieren nach demselben Prinzip: eine clientseitig gespeicherte, maskierte Kennung, die bei jeder GraphQL-Anfrage mitgeschickt wird. Die Compare-uid identifiziert die Vergleichsliste, die Quote-Id identifiziert den Warenkorb, beide sind unabhängig voneinander gültig.
4Wie viele Produkte lassen sich gleichzeitig vergleichen?
Das ist über die Magento-Konfiguration steuerbar und nicht technisch fest verdrahtet. In der Praxis sind vier bis fünf Produkte sinnvoll, mehr macht die Vergleichstabelle selbst auf großen Bildschirmen schnell unübersichtlich.
5Was passiert mit der Vergleichsliste eines Gasts nach dem Login?
Ohne expliziten Merge-Aufruf geht sie verloren, weil die Kundensitzung eine eigene, separate Liste referenziert. Die Mutation assignCompareListToCustomer führt beide Listen zusammen und muss im Login-Flow aktiv aufgerufen werden.
6Bricht das Compare-Feature den Full Page Cache?
Nein, solange die gesamte Logik über GraphQL-Mutationen und einen clientseitigen Alpine-Store läuft, bleibt die eigentliche Produktseite vollständig cachefähig. Es sind keine ESI-Blöcke oder Cache-Ausnahmen für den Compare-Zähler nötig.
7Wie stelle ich die Vergleichstabelle auf kleinen Bildschirmen dar?
Horizontales Scrollen mit einer sticky positionierten ersten Spalte für die Attributnamen funktioniert zuverlässig. Bei mehr als drei Produkten ist ein Alpine-gesteuertes Akkordeon je Produkt oft die verständlichere Alternative zur klassischen Tabelle.
8Wie hebe ich unterschiedliche Attributwerte in der Tabelle hervor?
Clientseitig in Alpine, sobald die Produktdaten aus der GraphQL-Antwort vorliegen: Pro Zeile wird geprüft, ob alle Werte identisch sind, bei Abweichung wird eine dezente Hintergrundfarbe gesetzt. Numerische Werte sollten dabei normalisiert statt nur als Text verglichen werden.
9Muss ich den Compare-Store in ein CSP-Inline-Script registrieren?
Ja, jedes Inline-Script, das den Alpine-Store initialisiert, muss über hyvaCsp registerInlineScript freigegeben werden. Ohne diese Registrierung blockiert die Content Security Policy das Script, und die Compare-Buttons reagieren nicht auf Klicks.
10Kann ich das Compare-Widget mit dem bestehenden Wishlist-Button kombinieren?
Ja, beide lassen sich im selben Produktkarten-Override nebeneinander platzieren, solange jeder Button seinen eigenen Alpine-Store anspricht. Wichtig ist nur, dass sich die x-data-Bereiche nicht überschneiden und jeder Button seinen eigenen Produkt-uid korrekt referenziert.