Warenkorbseite im Hyvä-Theme anpassen: Cart Page statt Minicart
AI generated
Hyvä
phtml
Hyvä Theme · Warenkorbseite
Die volle Warenkorbseite im Hyvä-Theme anpassen
Cart-Page-Template, Alpine-Interaktionen und Gutschein-UX statt Minicart

Die Minicart deckt den schnellen Blick in den Warenkorb ab, die eigentliche Cart-Page ist aber ein eigenständiges Template mit eigenen Anforderungen. Mengenänderungen ohne Reload, ein durchdachter Cross-Sell-Bereich und eine Gutschein-Eingabe, die Fehler klar kommuniziert, entscheiden hier oft über Kaufabbruch oder Checkout-Start.

14 Min. Lesezeit Cart Page Template updateCartItems Gutschein UX Cross-Sell GraphQL Mutations

1. Unterschied zwischen Minicart und Cart-Page-Template

Die Minicart ist ein Dropdown-Fragment im Header, das über den globalen Cart-Store aus dem Hyvä-GraphQL-Modul gespeist wird und primär für einen schnellen Überblick gedacht ist. Die Cart-Page dagegen ist eine eigenständige Route unter checkout/cart mit eigenem Layout-Handle checkout_cart_index und eigenem Template, das deutlich mehr Platz für Produktbilder, Artikeloptionen, Versandkosten-Schätzung und einen Cross-Sell-Bereich bietet.

In der Praxis teilen sich Minicart und Cart-Page zwar denselben Alpine-Cart-Store und dieselben GraphQL-Mutationen, aber nicht dasselbe Template. Wer versucht, die Cart-Page einfach als vergrößerte Minicart zu bauen, verschenkt den zusätzlichen Platz für Informationen, die auf der eigenständigen Seite Sinn ergeben, aber im Header-Dropdown stören würden, etwa ausführliche Artikeloptionen oder eine Geschenkverpackungs-Auswahl.

2. Die Template-Struktur der Cart-Page in Hyvä

Die relevanten Dateien liegen im Hyvä-Checkout-Modul unter Magento_Checkout/templates/cart, das eigentliche Grundgerüst wird über checkout_cart_index.xml als Layout-Handle gesteuert. Anders als in Luma gibt es keinen Knockout-Container mit dynamisch nachgeladenen UI-Components, sondern ein serverseitig gerendertes phtml-Grundgerüst, das seinen initialen Zustand aus einer GraphQL-Query beim Seitenaufruf lädt und danach ausschließlich über Alpine und weitere GraphQL-Mutationen aktualisiert wird.

Für Anpassungen empfiehlt sich, das bestehende Template zu überschreiben statt komplett neu zu schreiben, weil Hyvä bereits die grundlegende Cart-Item-Iteration, die Zusammenfassungs-Box und die Gutschein-Sektion sauber strukturiert vorgibt. Eigene Ergänzungen wie ein Cross-Sell-Bereich lassen sich als zusätzlicher Block über die Layout-XML einhängen, ohne die bestehende Struktur zu zerschlagen.


<!-- app/design/frontend/Mironsoft/default/Magento_Checkout/layout/checkout_cart_index.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <block class="Magento\Framework\View\Element\Template"
                   name="cart.crosssell"
                   template="Magento_Checkout::cart/crosssell.phtml"
                   after="checkout.cart.form" />
        </referenceContainer>
    </body>
</page>

3. Mengenänderung als Alpine-Interaktion ohne Seiten-Reload

Ein voller Seiten-Reload bei jeder Mengenänderung war in Luma die Regel und ist im Hyvä-Zeitalter ein klarer UX-Rückschritt. Stattdessen wird das Mengenfeld an eine Alpine-Methode gebunden, die nach kurzer Verzögerung, dem sogenannten Debouncing, die Mutation updateCartItems auslöst und danach nur die betroffenen Bereiche neu rendert: Zeilensumme, Gesamtsumme und Minicart-Zähler.

Wichtig ist ein klarer Ladezustand während der Anfrage, sonst kann ein ungeduldiger Kunde mehrfach klicken und dadurch mehrere überlappende Requests auslösen, deren Antworten in falscher Reihenfolge eintreffen können. Ein einfaches x-data-Flag pro Zeile, das den Eingabebereich während der Anfrage deaktiviert, verhindert dieses Race-Condition-Problem zuverlässig.


function cartItem(itemUid, initialQty) {
  return {
    qty: initialQty,
    updating: false,
    debounceTimer: null,

    onQtyChange() {
      clearTimeout(this.debounceTimer);
      this.debounceTimer = setTimeout(() => this.updateQty(), 500);
    },

    async updateQty() {
      this.updating = true;
      try {
        await this.$store.cart.updateItems([
          { cart_item_uid: itemUid, quantity: this.qty },
        ]);
      } catch (error) {
        this.$store.cart.showError('Menge konnte nicht aktualisiert werden.');
      } finally {
        this.updating = false;
      }
    },
  };
}

4. Artikel entfernen ohne volle Seiten-Aktualisierung

Das Entfernen eines Artikels folgt demselben Muster wie die Mengenänderung, mit dem Unterschied, dass hier ein sofortiges optisches Feedback wichtiger ist als bei einer reinen Mengenänderung. Ein kurzer Ausblend-Übergang, bevor der Artikel tatsächlich aus dem DOM entfernt wird, signalisiert dem Kunden deutlich, dass die Aktion erkannt wurde, noch bevor die GraphQL-Antwort überhaupt zurückkommt.

Bei leerem Warenkorb nach dem letzten Entfernen muss die Cart-Page auf eine eigene Leer-Zustand-Ansicht umschalten, statt eine leere Tabelle anzuzeigen. Dieser Zustand wird am saubersten reaktiv aus der Artikelanzahl im Cart-Store abgeleitet, statt ihn separat zu verwalten, damit er automatisch korrekt bleibt, egal ob der letzte Artikel über die Cart-Page oder über die Minicart entfernt wurde.

5. Gutschein-Eingabe UX ohne Frustpunkte

Die Gutschein-Eingabe ist eine der Stellen im Checkout-Trichter, an der schlechte Fehlerkommunikation besonders viel Vertrauen kostet. Ein abgelaufener oder falsch geschriebener Code darf nicht als generische Fehlermeldung erscheinen, sondern sollte, soweit die GraphQL-Antwort das hergibt, den konkreten Grund benennen, etwa abgelaufen, nicht mit anderen Aktionen kombinierbar oder Mindestbestellwert nicht erreicht.

Ebenso wichtig ist ein sichtbarer Ladezustand während der Prüfung, da applyCouponToCart serverseitig eine vollständige Neuberechnung der Bestellsumme samt Steuern und Versand auslöst und dadurch spürbar länger dauern kann als eine reine Mengenänderung. Ein deaktivierter Button mit Lade-Icon während der Anfrage verhindert Mehrfach-Submits mit demselben Code.


<div x-data="{ code: '', applying: false, error: '' }" class="flex flex-col gap-2">
  <div class="flex gap-2">
    <input type="text" x-model="code" placeholder="Gutscheincode" class="border p-2">
    <button
      type="button"
      x-bind:disabled="applying || code.length === 0"
      x-on:click="applying = true; applyCoupon(code)
        .catch(e => error = e.message)
        .finally(() => applying = false)"
    >
      <span x-show="!applying">Einlösen</span>
      <span x-show="applying">Wird geprüft...</span>
    </button>
  </div>
  <p class="text-sm text-red-600" x-show="error" x-text="error"></p>
</div>

6. Cross-Sell-Bereich auf der Warenkorbseite

Magentos klassisches Cross-Sell-Attribut, im Backend unter Verwandte Produkte gepflegt, wird traditionell direkt auf der Cart-Page unterhalb der Artikelliste angezeigt und ist eine der wenigen produktbezogenen Empfehlungen, die tatsächlich an der Kaufentscheidung dranhängen, da der Kunde sich bereits im Kaufprozess befindet. Die GraphQL-Query cart mit dem Feld items und darin verschachteltem cross_sell_products liefert die passenden Vorschläge direkt zusammen mit dem restlichen Warenkorb-Zustand.

Wichtig für die Conversion ist eine unaufdringliche Platzierung: Der Cross-Sell-Bereich gehört unterhalb der eigentlichen Artikelliste und Zusammenfassung, niemals oberhalb des Weiter-zur-Kasse-Buttons, sonst wirkt er wie ein zusätzliches Hindernis statt einer Zusatzoption. Ein einfacher Hinzufügen-Button pro Cross-Sell-Produkt, der dieselbe addProductsToCart-Mutation wie der Rest der Seite nutzt, hält die Implementierung konsistent.

7. Fehlerbehandlung und Optimistic UI

Zwischen dem Laden der Cart-Page und dem tatsächlichen Absenden einer Mengenänderung kann sich der Lagerbestand geändert haben, gerade bei stark nachgefragten Produkten mit wenigen verbleibenden Einheiten. Die Antwort von updateCartItems enthält in diesem Fall user_errors mit einer konkreten Fehlermeldung, die direkt an der betroffenen Zeile angezeigt werden sollte, statt als generischer Alert am oberen Seitenrand.

Ein optimistisches UI, das die neue Menge sofort anzeigt, bevor die Serverantwort eintrifft, verbessert die gefühlte Geschwindigkeit deutlich, birgt aber das Risiko, bei einem Fehler kurzzeitig einen falschen Zustand zu zeigen. Der pragmatische Mittelweg ist ein sichtbarer, aber dezenter Ladezustand statt echtem Optimistic-UI, der die wahrgenommene Wartezeit reduziert, ohne das Risiko falscher Zwischenzustände einzugehen.

8. Performance-Überlegungen für die Cart-Page

Die Cart-Page ist naturgemäß personalisiert und deshalb nicht sinnvoll über den Full Page Cache auslieferbar, das grundlegende HTML-Gerüst lässt sich aber trotzdem cachen, solange der eigentliche Inhalt vollständig über GraphQL nachgeladen wird. Wichtig ist, das Debouncing bei Mengenänderungen konsequent umzusetzen, da jede Eingabe ohne Verzögerung sonst eine eigene GraphQL-Anfrage auslöst und bei schnellem Tippen unnötig viele Requests parallel abfeuert.

Der Cross-Sell-Bereich sollte seine Produktbilder ebenfalls mit loading lazy versehen, da er meist erst nach dem initialen Viewport sichtbar wird. Bei Warenkörben mit vielen Positionen lohnt sich außerdem, die Cross-Sell-Query erst nach dem initialen Rendern der Artikelliste nachzuladen, statt sie in dieselbe GraphQL-Anfrage wie den Warenkorb-Inhalt selbst zu packen, um die Zeit bis zur ersten sichtbaren Artikelliste zu verkürzen.

9. Typische Fehler bei der Cart-Page-Anpassung

Ein häufiger Fehler ist, Minicart und Cart-Page als komplett getrennte Systeme zu implementieren, obwohl beide denselben Cart-Store nutzen sollten. Das führt zu Inkonsistenzen, etwa wenn eine Mengenänderung auf der Cart-Page nicht sofort im Minicart-Zähler im Header sichtbar wird, weil zwei unabhängige Zustände gepflegt werden, die nicht miteinander synchronisiert sind.

Ebenso problematisch ist fehlendes Debouncing bei Mengenfeldern, wodurch jede Tastatureingabe sofort eine GraphQL-Mutation auslöst und der Server mit überflüssigen Requests belastet wird. Und schließlich wird die Leer-Zustand-Ansicht der Cart-Page oft vergessen oder nur unvollständig gebaut, sodass Kunden nach dem Entfernen des letzten Artikels eine leere, aber strukturell noch vorhandene Tabelle statt einer klaren Weiter-einkaufen-Aufforderung sehen.

Bereich Minicart Cart-Page Geteilte Grundlage
Datenquelle Alpine Cart-Store Alpine Cart-Store GraphQL cart Query
Layout-Handle default (Header-Fragment) checkout_cart_index keine Überschneidung
Mengenänderung Eingeschränkt bis gar nicht Vollwertig mit Debouncing updateCartItems Mutation
Gutschein-Eingabe Selten sinnvoll Zentraler UX-Baustein applyCouponToCart Mutation
Cross-Sell Nicht vorgesehen Unterhalb der Artikelliste cross_sell_products Feld

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

Warenkorbseite in Hyvä: Das Wichtigste auf einen Blick

Eigenständiges Template

Cart-Page ist mehr als eine große Minicart, mit eigenem Layout-Handle und mehr Platz für Details.

Kein Reload nötig

Mengenänderung und Entfernen laufen über GraphQL-Mutationen und aktualisieren nur betroffene Bereiche.

Gutschein-Fehler konkret benennen

Generische Fehlermeldungen bei falschem Code kosten unnötig Vertrauen im Checkout-Trichter.

Geteilter Cart-Store

Minicart und Cart-Page müssen denselben Zustand nutzen, sonst laufen Zähler und Inhalte auseinander.

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

1Ist die Cart-Page in Hyvä nur eine vergrößerte Minicart?
Nein, sie ist ein eigenständiges Template mit eigenem Layout-Handle checkout_cart_index und deutlich mehr Platz für Artikeloptionen, Versandkosten-Schätzung und Cross-Sell. Beide teilen sich aber denselben Alpine-Cart-Store und dieselben GraphQL-Mutationen.
2Warum sollte eine Mengenänderung nicht sofort bei jedem Tastendruck ausgelöst werden?
Ohne Debouncing löst jede Tastatureingabe eine eigene GraphQL-Mutation aus, was bei schnellem Tippen unnötig viele parallele Requests erzeugt. Eine kurze Verzögerung von etwa 500 Millisekunden nach der letzten Eingabe reduziert die Serverlast deutlich.
3Welche Mutation wird für Mengenänderungen und Entfernen von Artikeln genutzt?
Beides läuft über updateCartItems, wobei eine Mengenangabe von null einem Entfernen des Artikels entspricht. Alternativ steht auch eine dedizierte removeItemFromCart-Mutation zur Verfügung, je nachdem, welche im Cart-Store bereits implementiert ist.
4Wie sollten Fehlermeldungen bei einem ungültigen Gutscheincode aussehen?
Konkret statt generisch: Soweit die GraphQL-Antwort den Grund liefert, etwa abgelaufen oder Mindestbestellwert nicht erreicht, sollte genau dieser Text angezeigt werden. Eine pauschale Meldung wie Code ungültig verunsichert Kunden unnötig.
5Wo sollte der Cross-Sell-Bereich auf der Cart-Page platziert werden?
Immer unterhalb der Artikelliste und der Zusammenfassung, niemals oberhalb des Weiter-zur-Kasse-Buttons. Eine Platzierung darüber wirkt wie ein zusätzliches Hindernis im Checkout-Trichter statt einer freiwilligen Zusatzoption.
6Warum ist die Cart-Page nicht über den Full Page Cache auslieferbar?
Weil ihr Inhalt naturgemäß personalisiert ist und sich mit jedem Warenkorb-Inhalt ändert. Das grundlegende HTML-Gerüst lässt sich trotzdem cachen, solange der eigentliche Inhalt vollständig über GraphQL nachgeladen und clientseitig gerendert wird.
7Was ist echtes Optimistic UI und warum wird davon eher abgeraten?
Optimistic UI zeigt eine Änderung sofort an, bevor die Serverantwort eintrifft, was schneller wirkt, aber bei einem Fehler kurzzeitig einen falschen Zustand anzeigt. Ein sichtbarer, dezenter Ladezustand ist meist der pragmatischere Mittelweg.
8Wie verhindere ich Inkonsistenzen zwischen Minicart-Zähler und Cart-Page?
Indem beide denselben Alpine-Cart-Store als einzige Wahrheitsquelle verwenden, statt getrennte Zustände zu pflegen. Jede Änderung auf der Cart-Page muss automatisch im globalen Store landen, damit der Minicart-Zähler im Header sofort mitzieht.
9Was zeigt die Cart-Page an, wenn der letzte Artikel entfernt wurde?
Eine eigene Leer-Zustand-Ansicht mit einer Aufforderung zum Weitereinkaufen, statt einer leeren, aber strukturell noch vorhandenen Tabelle. Dieser Zustand sollte reaktiv aus der Artikelanzahl im Cart-Store abgeleitet werden.
10Sollten Cross-Sell-Produktbilder lazy geladen werden?
Ja, da der Cross-Sell-Bereich meist erst unterhalb des initialen Viewports sichtbar wird. loading lazy auf den Produktbildern verhindert, dass diese die Ladezeit der oberen Warenkorb-Sektion unnötig verschlechtern.