Versandmethoden im Hyvä-Checkout anpassen und erweitern
AI generated
Hyvä
phtml
Hyvä-Checkout · Alpine.js · GraphQL · Tailwind CSS v4
Versandmethoden im Hyvä-Checkout anpassen
Darstellung, Gruppierung und eigene Carrier-Modelle

Die Darstellung der Versandmethoden im Checkout entscheidet oft über Kaufabbruch oder Abschluss. Im Hyvä-Checkout ersetzt eine schlanke Alpine.js-Komponente, die ihre Daten per GraphQL availableShippingMethods query lädt, die Knockout shipping-rates-Komponente aus Luma. Wer diese Komponente anpassen will, arbeitet mit shipping-methods.phtml, ViewModels und eigenen Carrier-Modellen statt mit uiComponents, Observables und RequireJS-Modulen, und bekommt dafür Kartenansichten, Liefertermine, Freigrenzen-Hinweise und Click-and-Collect als reaktive, wartbare Bausteine.

19 Min. Lesezeit Alpine.js · GraphQL · ViewModels · Carrier-Modelle Magento 2.4.8-p4 · Hyvä Themes · PHP 8.4

1. Wie Versandmethoden im Hyvä-Checkout gerendert werden

Die Darstellung der Versandmethoden im Hyvä-Checkout liegt in einer einzigen Template-Datei: app/design/frontend/Mironsoft/default/Magento_Checkout/templates/checkout/shipping-methods.phtml, die das Parent-Theme hyva-themes/magento2-default-theme-csp überschreibt. Der Container trägt x-data="initShippingMethods()", eine Alpine-Factory-Funktion, die den kompletten Zustand hält: geladene Methoden, ausgewählte method_code, Ladezustand und Fehlertext. Es gibt kein uiComponent-Registry, keine shipping-rates.js und keine Knockout-Templates mit data-bind-Attributen mehr, die im Luma-Checkout die Versandarten rendern.

Die Daten kommen nicht mehr aus dem serverseitig aufgebauten Checkout-Datenmodell, sondern aus einer GraphQL availableShippingMethods query, die gegen das shipping_addresses-Feld der cart-Query läuft. Sobald der Kunde eine Adresse einträgt oder ändert, setzt die Alpine-Komponente die Mutation setShippingAddressesOnCart ab und liest anschließend available_shipping_methods mit carrier_code, method_code, carrier_title, method_title und amount aus der Antwort. Diese Trennung aus Adress-Mutation und Methoden-Abfrage ist der zentrale Unterschied zur Knockout shipping-rates-Komponente, die Adresse und Versandarten in einem einzigen, schwer entkoppelbaren Observable-Baum hielt.

Für die Darstellung der Versandmethoden im Checkout bleibt der Hyvä-Block-Mechanismus vollständig erhalten: $block->getChildNames() wird weiterhin iteriert, damit zusätzliche Blöcke wie ein Filialfinder oder ein Freigrenzen-Banner an definierten Stellen einhängen können. Alpine übernimmt nur die client-seitige Reaktivität rund um die Methodenauswahl, nicht das serverseitige Block- und Layout-System von Magento. Wer diese Trennung respektiert, kann die Komponente erweitern, ohne die Layout-XML-Steuerung des Checkouts aufzugeben.


# GraphQL query used by the Alpine shipping methods component
# Triggered after setShippingAddressesOnCart succeeds

query CheckoutShippingMethods($cartId: String!) {
  cart(cart_id: $cartId) {
    shipping_addresses {
      available_shipping_methods {
        carrier_code
        carrier_title
        method_code
        method_title
        amount {
          value
          currency
        }
        price_excl_tax {
          value
        }
        price_incl_tax {
          value
        }
        available
        error_message
      }
      selected_shipping_method {
        carrier_code
        method_code
      }
    }
  }
}

# Mutation fired when the customer selects a shipping method card
mutation SetShippingMethod($cartId: String!, $carrierCode: String!, $methodCode: String!) {
  setShippingMethodsOnCart(
    input: {
      cart_id: $cartId
      shipping_methods: [{ carrier_code: $carrierCode, method_code: $methodCode }]
    }
  ) {
    cart {
      shipping_addresses {
        selected_shipping_method {
          carrier_code
          method_code
        }
      }
    }
  }
}

2. Eigene Darstellung der Versandmethoden: von der Radio-Liste zur Tailwind-Kartenansicht

Der Standard-Checkout stellt Versandmethoden als schlichte Radio-Liste dar, was bei mehr als zwei oder drei Optionen schnell unübersichtlich wird. Eine gebräuchliche Anpassung der Versandmethoden im Hyvä-Checkout ist die Umwandlung dieser Liste in eine Tailwind-Kartenansicht: Jede Methode bekommt eine eigene <label>-Karte mit Carrier-Icon, Titel, Preis und Lieferzeit, während das eigentliche <input type="radio"> visuell versteckt, aber für Tastatur und Screenreader erhalten bleibt (class="sr-only peer"). Der ausgewählte Zustand wird rein über Tailwind-Peer-Klassen gesteuert: peer-checked:border-orange-500 peer-checked:ring-2 peer-checked:ring-orange-200.

Icons pro Carrier werden, wie im gesamten Theme gefordert, als inline <svg> eingebunden, niemals als Icon-Font. Jeder Carrier-Code (dhlpaket, dpd, ups, flatrate für die eigene Abholoption) bekommt ein zugeordnetes SVG-Fragment, das über eine kleine PHP-Zuordnung im ViewModel aufgelöst wird, statt das Icon im Template hart zu verdrahten. So bleibt die Darstellung dieser Komponente erweiterbar, wenn ein neuer Carrier hinzukommt, ohne das Kern-Template anzufassen.

Die Lieferzeit wird als zusätzliche Zeile in der Karte angezeigt, zum Beispiel "Lieferung: Mo, 27. Juli" statt eines nichtssagenden "3-5 Werktage". Diese Angabe stammt entweder direkt aus einem eigenen GraphQL-Feld am Carrier oder wird, wie in Abschnitt 5 beschrieben, clientseitig aus Cutoff-Zeit und Lagerbestand berechnet. Wichtig ist, dass Karten mit available: false ausgegraut, aber nicht ausgeblendet werden, mit einer kurzen Begründung wie "Nicht verfügbar für diese Adresse" statt sie stillschweigend verschwinden zu lassen.


<!-- app/design/frontend/Mironsoft/default/Magento_Checkout/templates/checkout/shipping-methods.phtml -->
<?php
/** @var \Magento\Checkout\Block\Checkout\LayoutProcessorInterface $block */
/** @var \Hyva\Theme\ViewModel\HyvaCsp $hyvaCsp */
$hyvaCsp = $viewModels->require(\Hyva\Theme\ViewModel\HyvaCsp::class);
?>
<div x-data="initShippingMethods()" x-init="fetchMethods()" class="space-y-3">
  <template x-if="loading">
    <div class="text-sm text-slate-500 p-4">Versandmethoden werden geladen &hellip;</div>
  </template>

  <template x-if="!loading && methods.length === 0">
    <div class="rounded-xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
      Für diese Adresse ist aktuell keine Versandmethode verfügbar.
    </div>
  </template>

  <template x-for="method in sortedMethods" :key="method.carrier_code + method.method_code">
    <label
      class="flex items-center gap-4 p-4 rounded-2xl border border-slate-200 cursor-pointer transition-colors hover:border-orange-300"
      :class="{ 'opacity-50 cursor-not-allowed': !method.available }"
    >
      <input
        type="radio"
        name="shipping-method"
        class="sr-only peer"
        :value="method.carrier_code + '_' + method.method_code"
        :disabled="!method.available"
        x-model="selected"
        @change="selectMethod(method)"
      >
      <span class="w-10 h-10 flex-shrink-0 rounded-lg bg-slate-50 flex items-center justify-center peer-checked:bg-orange-50">
        <!-- Carrier icon resolved via ViewModel, inline SVG, no icon font -->
        <?= /* @noEscape */ '' ?>
      </span>
      <span class="flex-1">
        <span class="block text-sm font-semibold text-slate-800" x-text="method.method_title"></span>
        <span class="block text-xs text-slate-500" x-text="method.deliveryEstimate"></span>
        <template x-if="!method.available">
          <span class="block text-xs text-red-600 mt-1" x-text="method.error_message"></span>
        </template>
      </span>
      <span class="text-sm font-bold text-slate-900" x-text="formatPrice(method.amount.value)"></span>
    </label>
  </template>
</div>
<?php $hyvaCsp->registerInlineScript(); ?>

3. Versandmethoden nach Carrier gruppieren und sortieren

Sobald mehrere Carrier gleichzeitig aktiv sind, etwa DHL für den Standardversand und ein eigener Express-Carrier, wird die Reihenfolge der Versandmethoden im Checkout zu einer echten Business-Entscheidung: Welche Methode soll zuerst erscheinen, welche soll optisch hervorgehoben werden? Diese Sortierlogik gehört nicht ins Template und schon gar nicht in eine Alpine-x-for-Direktive mit Inline-Vergleichsfunktion, sondern in ein PHP-ViewModel, das die Priorität pro Carrier-Code als konfigurierbaren Wert bereitstellt.

Ein ShippingMethodSortOrder-ViewModel liest eine Konfigurationstabelle aus system.xml aus, die jedem carrier_code eine Sortierposition zuweist, und reicht diese Reihenfolge als JSON-Array an die Alpine-Komponente weiter. Die Komponente sortiert die von der GraphQL-Query gelieferten Methoden dann rein clientseitig anhand dieses Arrays, bevorzugt bevorzugte Versandarten wie Express oder Click-and-Collect nach oben und degradiert langsame oder teure Optionen ans Ende der Liste. Diese Trennung hält das Template lesbar und macht die Priorisierung über den Admin konfigurierbar, ohne Deployment.

Für die Gruppierung nach Carrier, etwa wenn ein Carrier mehrere Methoden anbietet (Standard, Express, Same-Day), reicht eine einfache reduce()-Operation im Alpine-State, die die flache Liste aus available_shipping_methods in ein nach carrier_code gruppiertes Objekt überführt. Die Kartenansicht zeigt dann pro Carrier eine Kopfzeile mit Logo, darunter die zugehörigen Methoden als Unterkarten, was gerade bei vielen aktiven Versandarten die Übersichtlichkeit deutlich verbessert.

4. Eigene Versandart (Carrier-Model) korrekt im Checkout anzeigen lassen

Ein eigener Carrier für Versandmethoden im Checkout, etwa für Express-Versand oder Click-and-Collect, wird als PHP-Klasse implementiert, die Magento\Shipping\Model\Carrier\AbstractCarrier erweitert und Magento\Shipping\Model\Carrier\CarrierInterface implementiert. Die zentrale Methode collectRates(RateRequest $request) liefert ein Magento\Shipping\Model\Rate\Result-Objekt mit einer oder mehreren Method-Instanzen zurück, jede mit eigenem method_code, method_title und Preis. Genau diese Werte tauchen später unverändert in der GraphQL-Antwort unter available_shipping_methods auf, weshalb die Titel hier bereits so formuliert werden sollten, wie sie im Frontend erscheinen sollen.

Die Konfiguration des Carriers läuft klassisch über etc/config.xml mit Default-Werten unter carriers/<code>/active, carriers/<code>/title und carriers/<code>/name, ergänzt um ein system.xml, das diese Werte im Admin unter Stores > Configuration > Sales > Shipping Methods editierbar macht. Wichtig für die korrekte Zuordnung im Checkout: getAllowedMethods() muss exakt die method_code-Werte zurückgeben, die auch in collectRates() erzeugt werden, sonst tauchen die Methoden im Checkout inkonsistent oder gar nicht auf, weil der GraphQL-Resolver die Methode nicht der Konfiguration zuordnen kann.


<?php
declare(strict_types=1);

namespace Mironsoft\ShippingExtend\Model\Carrier;

use Magento\Quote\Model\Quote\Address\RateRequest;
use Magento\Shipping\Model\Carrier\AbstractCarrier;
use Magento\Shipping\Model\Carrier\CarrierInterface;
use Magento\Shipping\Model\Rate\Result;
use Magento\Shipping\Model\Rate\ResultFactory;
use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory;

/**
 * Custom express shipping carrier surfaced in the Hyva checkout shipping methods list.
 */
final class ExpressCarrier extends AbstractCarrier implements CarrierInterface
{
    /**
     * @var string Carrier code referenced in config.xml and the GraphQL resolver.
     */
    protected $_code = 'mironsoft_express';

    /**
     * @param ResultFactory $rateResultFactory Builds the shipping rate result.
     * @param MethodFactory $rateMethodFactory Builds individual shipping methods.
     * @param array $data Additional carrier data.
     */
    public function __construct(
        private readonly ResultFactory $rateResultFactory,
        private readonly MethodFactory $rateMethodFactory,
        array $data = [],
    ) {
        parent::__construct($data);
    }

    /**
     * Collects the express shipping rate, title matches what the Hyva checkout card renders.
     *
     * @param RateRequest $request The rate request with destination and cart data.
     * @return Result|bool
     */
    public function collectRates(RateRequest $request): Result|bool
    {
        if (!$this->getConfigFlag('active')) {
            return false;
        }

        /** @var Result $result */
        $result = $this->rateResultFactory->create();

        $method = $this->rateMethodFactory->create();
        $method->setCarrier($this->_code);
        $method->setCarrierTitle($this->getConfigData('title'));
        $method->setMethod('express');
        $method->setMethodTitle('Express-Versand (Lieferung morgen)');
        $method->setPrice((float) $this->getConfigData('price'));
        $method->setCost((float) $this->getConfigData('price'));

        $result->append($method);

        return $result;
    }

    /**
     * Must match the method codes produced by collectRates() exactly.
     *
     * @return string[]
     */
    public function getAllowedMethods(): array
    {
        return ['express' => $this->getConfigData('name')];
    }
}

<!-- app/code/Mironsoft/ShippingExtend/etc/config.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/config.xsd">
    <default>
        <carriers>
            <mironsoft_express>
                <active>1</active>
                <title>Mironsoft Express</title>
                <name>Express-Versand</name>
                <price>9.90</price>
                <cutoff_time>14:00</cutoff_time>
                <sallowspecific>0</sallowspecific>
                <model>Mironsoft\ShippingExtend\Model\Carrier\ExpressCarrier</model>
            </mironsoft_express>
        </carriers>
    </default>
</config>

<!-- app/code/Mironsoft/ShippingExtend/etc/adminhtml/system.xml (excerpt) -->
<system>
    <section id="carriers">
        <group id="mironsoft_express" translate="label" type="text" sortOrder="15" showInDefault="1">
            <label>Mironsoft Express</label>
            <field id="active" translate="label" type="select" sortOrder="1" showInDefault="1">
                <label>Enabled</label>
                <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
            </field>
            <field id="price" translate="label" type="text" sortOrder="3" showInDefault="1">
                <label>Price</label>
            </field>
            <field id="cutoff_time" translate="label" type="text" sortOrder="4" showInDefault="1">
                <label>Order cutoff time for next-day delivery</label>
            </field>
        </group>
    </section>
</system>

5. Dynamische Anzeige: Liefertermin-Berechnung und Express-Versand-Hinweis

Ein Liefertermin wie "Lieferung: Do, 24. Juli" ist für Kunden greifbarer als "3-5 Werktage" und ist eine der wirkungsvollsten Anpassungen an Versandmethoden im Checkout. Die Berechnung berücksichtigt drei Faktoren: den konfigurierten cutoff_time-Wert des Carriers aus Abschnitt 4, den aktuellen Lagerbestand der Artikel im Warenkorb, und die Laufzeit der Versandart selbst (ein Tag für Express, zwei bis vier Werktage für Standard). Diese Logik gehört in ein PHP-ViewModel, das ein einfaches Datum als ISO-String an Alpine übergibt, statt sie im JavaScript nachzubauen und dabei Zeitzonen- oder Feiertagslogik zu duplizieren.

Für Artikel, die erst nachbestellt werden müssen, verschiebt sich der Liefertermin entsprechend, was clientseitig über einen zusätzlichen GraphQL-Wert je Warenkorbposition abgebildet wird, statt den kompletten Termin serverseitig für den gesamten Warenkorb zu berechnen und bei jeder Änderung neu zu laden. Der Express-Versand-Hinweis selbst ist rein reaktiver Alpine-State: Liegt die aktuelle Uhrzeit vor dem Cutoff, zeigt ein grüner Banner "Bestelle in den nächsten 2 Stunden 14 Minuten für Lieferung morgen", nach dem Cutoff verschiebt sich die Meldung automatisch auf den übernächsten Werktag.

Damit dieser Hinweis nicht bei jedem Render-Zyklus neu berechnet werden muss, läuft die Zeitdifferenz über einen einzigen setInterval im x-init-Hook der Komponente, der die verbleibende Zeit jede Minute aktualisiert, statt bei jedem Alpine-Tick neu zu rechnen. Diese Kombination aus serverseitig berechnetem Basisdatum und clientseitig tickendem Countdown ist die robusteste Lösung für dynamische Liefertermine im Hyvä-Checkout.

6. Versandkosten-Anzeige und Freigrenzen-Hinweise

Der Hinweis "Noch 24,50 € bis zum kostenlosen Versand" gehört zu den am stärksten konversionsfördernden Elementen der Versandmethoden-Darstellung im Checkout, weil er direkt zur Erhöhung des Warenkorbwerts motiviert. Die Berechnung basiert auf der Differenz aus dem konfigurierten Schwellenwert (freeshipping/free_shipping_subtotal) und dem aktuellen grand_total aus der Cart-Query, ausgegeben als reaktiver Alpine-Ausdruck: x-text="'Noch ' + formatPrice(threshold - cart.prices.grand_total.value) + ' bis zum kostenlosen Versand'". Sobald der Schwellenwert erreicht ist, wechselt die Anzeige automatisch zu einer Bestätigung wie "Kostenloser Versand ab jetzt aktiv" mit grünem Haken-Icon.

Die korrekte Preisformatierung ist bei Versandmethoden im Checkout kein Detail, sondern ein häufiger Fehlerquell: Der von der GraphQL-Query gelieferte amount.value ist ein reiner Zahlenwert ohne Währungsformatierung, weshalb eine gemeinsame formatPrice()-Hilfsfunktion in Alpine Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }) nutzt. Ob der Preis inklusive oder exklusive MwSt. angezeigt wird, hängt vom Store-Konfigurationswert tax/display/shipping ab, weshalb die Komponente je nach Konfiguration zwischen price_incl_tax und price_excl_tax aus der GraphQL-Antwort wechselt, statt einen der beiden Werte hart zu verdrahten.

Der Fortschrittsbalken zur Freigrenze selbst nutzt denselben reaktiven Mechanismus wie in der Minicart: x-bind:style="{ width: Math.min(100, (cart.prices.grand_total.value / threshold) * 100) + '%' }". Weil der Balken direkt an cart.prices.grand_total.value gebunden ist, aktualisiert er sich automatisch, sobald sich der Warenkorbwert durch eine Mengenänderung im Checkout verschiebt, ohne dass die Methodenliste neu geladen werden muss.


// app/design/frontend/Mironsoft/default/Magento_Checkout/web/js/shipping-methods.js
function initShippingMethods() {
  return {
    loading: true,
    methods: [],
    selected: null,
    freeShippingThreshold: 0,
    grandTotal: 0,

    get sortedMethods() {
      // Priority list injected server-side via ShippingMethodSortOrder ViewModel
      const priority = window.mironsoftShippingPriority || [];
      return [...this.methods].sort((a, b) => {
        const posA = priority.indexOf(a.carrier_code);
        const posB = priority.indexOf(b.carrier_code);
        return (posA === -1 ? 999 : posA) - (posB === -1 ? 999 : posB);
      });
    },

    get remainingForFreeShipping() {
      const remaining = this.freeShippingThreshold - this.grandTotal;
      return remaining > 0 ? remaining : 0;
    },

    async fetchMethods() {
      this.loading = true;
      const response = await this.graphqlQuery(SHIPPING_METHODS_QUERY, { cartId: this.getCartId() });
      const address = response.data.cart.shipping_addresses[0];
      this.methods = address.available_shipping_methods.map((method) => ({
        ...method,
        deliveryEstimate: this.calculateDeliveryEstimate(method),
      }));
      this.grandTotal = response.data.cart.prices.grand_total.value;
      this.loading = false;
    },

    calculateDeliveryEstimate(method) {
      const now = new Date();
      const cutoff = method.carrier_code === 'mironsoft_express' ? 14 : 23;
      const daysToAdd = now.getHours() < cutoff ? 1 : 2;
      const eta = new Date(now);
      eta.setDate(eta.getDate() + daysToAdd);
      return 'Lieferung: ' + eta.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'long' });
    },

    formatPrice(value) {
      return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value);
    },

    async selectMethod(method) {
      await this.graphqlMutation(SET_SHIPPING_METHOD_MUTATION, {
        cartId: this.getCartId(),
        carrierCode: method.carrier_code,
        methodCode: method.method_code,
      });
      window.dispatchEvent(new CustomEvent('shipping-method-selected', { detail: { method } }));
    },
  };
}

7. Click-and-Collect/Abholung im Checkout abbilden

Click-and-Collect wird technisch als ganz normale Versandmethode im Checkout abgebildet: ein eigener Carrier (mironsoft_pickup) mit Preis 0.00, der in der Kartenansicht neben DHL und Express erscheint, aber statt eines Preises den Hinweis "Kostenlose Abholung" zeigt. Sobald der Kunde diese Karte auswählt, blendet Alpine eine zusätzliche Filialauswahl-UI ein (x-show="selectedCarrier === 'mironsoft_pickup'"), die eine Liste verfügbarer Filialen mit Adresse, Öffnungszeiten und aktuellem Lagerbestand pro Artikel anzeigt.

Die Filialliste selbst stammt aus einer eigenen GraphQL-Query, die ein storeLocations-Feld gegen ein zweites, produktbezogenes Modul auflöst und für jede Filiale prüft, ob alle Artikel des aktuellen Warenkorbs dort verfügbar sind. Diese Prüfung läuft serverseitig im Resolver, nicht im Frontend, weil sie Zugriff auf die reale Lagerbestandstabelle (cataloginventory_stock_item beziehungsweise MSI-Sourcen bei Multi-Source-Inventory) benötigt, den das Frontend nicht direkt abfragen kann und sollte. Filialen mit unvollständiger Verfügbarkeit werden ausgegraut mit einem Hinweis wie "2 von 3 Artikeln verfügbar" statt komplett aus der Liste zu verschwinden.

Die Auswahl einer Filiale wird als zusätzliches custom_attribute auf den Warenkorb geschrieben, per setShippingMethodsOnCart-Mutation mit einem ergänzenden shipping_addresses-Feld für die Filial-ID. Erst wenn eine gültige Filiale mit vollständiger Verfügbarkeit ausgewählt ist, aktiviert sich der "Weiter"-Button im Checkout, gesteuert über denselben Alpine-State, der auch die übrigen Optionen im Checkout validiert.

8. Fehlerbehandlung bei nicht verfügbaren Versandmethoden

Liegt die Lieferadresse außerhalb des konfigurierten Liefergebiets, liefert die GraphQL-Query eine leere available_shipping_methods-Liste zurück, ganz ohne technischen Fehler. Der häufigste Anfängerfehler bei Versandmethoden im Checkout ist, diesen Fall im Template schlicht zu ignorieren, sodass der Kunde vor einem leeren Bereich steht und nicht versteht, warum er nicht weiterkommt. Die Alpine-Komponente muss diesen Zustand explizit abfangen (methods.length === 0) und eine verständliche, handlungsorientierte Meldung anzeigen, etwa "Für Ihre Adresse ist aktuell keine Lieferung möglich, bitte kontaktieren Sie unseren Kundenservice" statt einer leeren Fläche oder eines generischen Ladespinners, der nie verschwindet.

Ein zweiter Fehlerfall betrifft einzelne, temporär nicht verfügbare Versandmethoden, etwa wenn ein Carrier aufgrund von Wartungsarbeiten keine Rate liefert. Hier liefert Magento die Methode mit available: false und einem error_message-Feld, das direkt in der Karte angezeigt werden sollte, statt die Methode komplett zu verstecken. Das gibt dem Kunden Kontext, warum eine gewohnte Option fehlt, anstatt den Eindruck zu erwecken, der Checkout sei defekt.

Netzwerkfehler bei der GraphQL-Anfrage selbst, etwa ein Timeout oder ein 5xx-Response, benötigen eine dritte Fehlerebene: einen Retry-Button in der Alpine-Komponente (@click="fetchMethods()"), kombiniert mit einer klaren Meldung wie "Versandmethoden konnten nicht geladen werden. Erneut versuchen." Ohne diese explizite Fehlerbehandlung bleibt die Komponente im Ladezustand hängen, der Kunde bricht den Checkout ab, ohne dass ein technischer Fehler im Monitoring sichtbar würde.

9. Performance: Caching von Versandmethoden-Anfragen

Jede Adressänderung im Checkout löst potenziell eine neue Abfrage der Versandmethoden im Checkout aus, was bei unkontrollierter Implementierung schnell zu einer Flut paralleler GraphQL-Requests führt, insbesondere während der Kunde noch tippt. Ein x-model.debounce.400ms auf den Adressfeldern PLZ und Ort verhindert, dass bei jedem Tastendruck eine neue setShippingAddressesOnCart-Mutation samt anschließender Methoden-Abfrage abgesetzt wird. Erst nach 400 Millisekunden Inaktivität wird die Adresse tatsächlich übermittelt.

Zusätzlich lohnt sich ein einfacher clientseitiger Cache pro Adress-Hash: Ändert der Kunde die Hausnummer, aber PLZ und Land bleiben identisch, liefert derselbe Cache-Eintrag die zuletzt bekannten Versandmethoden zurück, ohne einen erneuten Request auszulösen, sofern sich der Warenkorbinhalt zwischenzeitlich nicht geändert hat. Der Cache-Schlüssel setzt sich aus PLZ, Land und einem Hash der Warenkorb-Item-UIDs zusammen, damit ein veränderter Warenkorb den Cache korrekt invalidiert. Ein einfaches In-Memory-Objekt in der Alpine-Komponente reicht dafür aus, ein localStorage-Cache ist wegen kurzer Gültigkeit meist nicht nötig.

Serverseitig lohnt sich außerdem, den GraphQL-Resolver für available_shipping_methods nicht bei jeder Anfrage die komplette Rate-Collection-Pipeline aller Carrier neu durchlaufen zu lassen, wenn sich nur ein einzelnes Attribut wie die Bestellnotiz geändert hat. Ein gezielter Cache-Tag auf Basis von Adresse und Warenkorbinhalt, kombiniert mit kurzer TTL von wenigen Sekunden, reduziert die Serverlast spürbar, ohne veraltete Versandarten anzuzeigen.

Aufgabe Knockout shipping-rates (Luma) Hyvä-Checkout mit Alpine/GraphQL Vorteil
Versandmethoden laden shipping-rates.js uiComponent + Observable-Baum GraphQL availableShippingMethods query Nur benötigte Felder, kein Component-Registry
Versandart auswählen Knockout data-bind change + Ajax-Reload der Seite x-model + setShippingMethodsOnCart Mutation Kein Full Reload, reaktives UI-Update
Lieferzeit anzeigen Eigene UI-Component + Knockout-Template pro Carrier ViewModel-Berechnung + x-text im Panel Zentrale Logik statt verstreuter Templates
Freigrenzen-Hinweis Separates Widget + manueller Ajax-Reload Reaktiver Alpine-State, x-bind:style Fortschrittsbalken Automatisches Update bei Mengenänderung
Leere Methodenliste behandeln Generische Fehlerseite oder leerer Bereich Eigene Fehlerkomponente mit Handlungsempfehlung Kunde versteht das Problem, kein Abbruch ohne Kontext

Der Vergleich zeigt, dass jede einzelne Anpassung an diesen Versandarten im Hyvä-Stack weniger bewegliche Teile benötigt als im Knockout-Äquivalent: kein Component-Registry, keine verschachtelten Observables, kein globales Event-System. Ein Alpine-Objekt plus gezielte GraphQL-Queries und -Mutationen genügt für Darstellung, Auswahl, Gruppierung und Fehlerbehandlung gleichermaßen.

10. Zusammenfassung

Versandmethoden im Hyvä-Checkout anzupassen bedeutet, an mehreren Ebenen gezielt anzusetzen: an shipping-methods.phtml mit ihrer Alpine-Factory-Funktion, an der GraphQL availableShippingMethods query samt eigener Carrier-Modelle, an ViewModels für Sortierlogik und Liefertermin-Berechnung, und an der Fehlerbehandlung für leere oder eingeschränkte Methodenlisten. Die Tailwind-Kartenansicht mit inline SVG-Icons ersetzt die schlichte Radio-Liste, Freigrenzen-Hinweise und Click-and-Collect laufen über denselben reaktiven Alpine-State wie die reguläre Methodenauswahl.

Wer die Darstellung der Versandmethoden im Checkout erweitern will, sollte konsequent zwischen serverseitiger Rate-Berechnung im Carrier-Model und clientseitiger Alpine-Reaktivität trennen: Der Carrier liefert Preis und Titel, das ViewModel liefert Sortierung und Liefertermin, Alpine steuert nur die Darstellung und Interaktion. Für die Performance gilt, Adressänderungen zu debouncen, Ergebnisse pro Adress-Hash zwischenzuspeichern und den GraphQL-Resolver serverseitig mit kurzer TTL zu cachen. So bleibt die Methodenauswahl schnell, korrekt und leicht erweiterbar.

Versandmethoden im Hyvä-Checkout anpassen: Das Wichtigste auf einen Blick

Rendering

shipping-methods.phtml mit x-data="initShippingMethods()", Daten per GraphQL availableShippingMethods query statt Knockout shipping-rates.

Darstellung & Gruppierung

Tailwind-Kartenansicht mit inline SVG-Icons, Sortierlogik im ViewModel statt im Template, Priorisierung bevorzugter Carrier.

Eigene Carrier & Click-and-Collect

AbstractCarrier-Implementierung mit config.xml, Filialauswahl und Verfügbarkeitsprüfung als eigene Versandart.

Fehler & Performance

Eigene Fehlerkomponente bei leerer Methodenliste, Debounce bei Adresseingabe, Caching pro Adress-Hash und kurze Server-TTL.

11. FAQ: Versandmethoden im Hyvä-Checkout anpassen

1Wie werden Versandmethoden im Hyvä-Checkout technisch gerendert?
Über eine Alpine.js-Komponente in shipping-methods.phtml mit Daten aus der GraphQL availableShippingMethods query, statt Knockout-Observables.
2Was ersetzt die Knockout shipping-rates-Komponente?
Die GraphQL availableShippingMethods query mit setShippingAddressesOnCart und setShippingMethodsOnCart Mutationen, ganz ohne Component-Registry.
3Wie stelle ich Versandmethoden als Kartenansicht dar?
label mit sr-only peer radio input, Peer-Klassen für den ausgewählten Zustand, Icons als inline SVG statt Icon-Font.
4Wie gruppiere und sortiere ich nach Carrier?
Sortierlogik im PHP-ViewModel mit konfigurierbarer Prioritätsliste, clientseitig auf die GraphQL-Antwort angewendet.
5Wie zeige ich eine eigene Versandart korrekt an?
AbstractCarrier mit collectRates(), config.xml und system.xml, getAllowedMethods() muss zu den erzeugten method_code-Werten passen.
6Wie berechne ich einen dynamischen Liefertermin?
ViewModel kombiniert Cutoff-Zeit, Lagerbestand und Versandlaufzeit zu einem Basisdatum, Alpine tickt den Countdown per setInterval.
7Wie zeige ich eine Freigrenze reaktiv an?
x-text mit der Differenz aus Schwellenwert und grand_total, formatiert mit Intl.NumberFormat, plus x-bind:style Fortschrittsbalken.
8Wie bilde ich Click-and-Collect ab?
Eigener Carrier mit Preis 0.00 plus Filialauswahl-UI, Verfügbarkeit wird serverseitig gegen den Lagerbestand geprüft.
9Wie behandle ich eine leere Methodenliste?
methods.length === 0 explizit abfangen und eine verständliche, handlungsorientierte Meldung anzeigen statt einer leeren Fläche.
10Wie vermeide ich unnötige GraphQL-Requests?
Debounce auf Adressfeldern, clientseitiger Cache pro Adress-Hash, serverseitiges Resolver-Caching mit kurzer TTL.

Mironsoft

Hyvä-Checkout, Alpine.js und GraphQL für Magento 2

Versandmethoden im Checkout sollen mehr können?

Wir passen die Darstellung eurer Versandmethoden im Hyvä-Checkout an: Tailwind-Kartenansicht, eigene Carrier-Modelle, Liefertermin-Berechnung, Freigrenzen-Hinweise und Click-and-Collect, alles auf Basis von Alpine.js und GraphQL.

Checkout-Audit

Analyse der bestehenden Versandmethoden-Darstellung auf Performance, Klarheit und Fehlerbehandlung

Carrier-Entwicklung

Eigene Versandarten, Click-and-Collect und Liefertermin-Logik als AbstractCarrier-Implementierung

Design-Anpassung

Tailwind-Kartenansicht, Icons und Freigrenzen-Hinweise passend zu eurem Corporate Design