Preisdarstellung im Hyvä-Theme: Staffelpreise und Countdown-Timer robust umsetzen
AI generated
Hyvä
phtml
Hyvä Theme
Preisdarstellung im Hyvä-Theme
Staffelpreise und Countdown-Timer robust umsetzen

Ein Mengenrabatt, der auf den ersten Blick verständlich ist, und ein Sale-Countdown, der nicht durch eine falsch gestellte Client-Uhr ausgehebelt wird, klingen nach Kleinigkeiten. In der Praxis entscheiden genau diese Details darüber, ob Kunden einer Preisangabe vertrauen oder sie als Trick abtun.

13 Min. Lesezeit Tier Price Countdown-Timer

1. Warum Preisdarstellung im Hyvä-Theme mehr ist als eine einzelne Zahl

Eine moderne Produktdetailseite zeigt selten nur einen einzelnen Preis: Staffelpreise für größere Bestellmengen, ein zeitlich befristeter Sonderpreis mit Countdown und eine klare Kennzeichnung, dass es sich überhaupt um eine Reduzierung handelt, müssen gleichzeitig und widerspruchsfrei dargestellt werden. Die GraphQL-Datenstruktur liefert dafür price_range, price_tiers und Sonderpreis-Felder als getrennte, unabhängig voneinander zu interpretierende Bausteine, aus denen das Theme ein stimmiges Gesamtbild formen muss.

Hyvä selbst liefert bewusst kein einziges, universelles Preis-UI-Pattern mit, denn die richtige Darstellung hängt stark vom Geschäftsmodell ab: Ein B2B-Katalog mit vielen Staffelstufen braucht eine andere Darstellung als ein B2C-Shop mit einem einzelnen Rabatt-Badge. Wird diese Entscheidung nicht bewusst getroffen, entstehen leicht Preisdarstellungen, die zwar technisch korrekt sind, aber für Kunden verwirrend wirken oder im schlimmsten Fall den falschen Eindruck einer nicht real reduzierten Ware erwecken.

2. Tier-Price-Daten aus GraphQL abrufen und korrekt aufbereiten

Die price_tiers-Liste eines Produkts liefert für jede Mengenschwelle den ab dieser Menge gültigen Endpreis, jedoch nicht direkt den prozentualen Rabatt gegenüber dem regulären Preis. Dieser muss im Frontend aus dem Verhältnis von Staffelpreis und regulärem Preis berechnet werden, wobei auf korrekte Rundung geachtet werden sollte, damit angezeigte Prozentwerte wie zwanzig statt neunzehn Komma sieben Prozent nicht den Eindruck einer ungenauen Berechnung erwecken.

Wichtig ist außerdem die Sortierung: Die API liefert Staffelstufen nicht zuverlässig in aufsteigender Reihenfolge der Mengenschwelle, weshalb das Theme die Liste vor der Darstellung explizit nach Menge sortieren sollte. Andernfalls kann eine Tabelle mit vertauschten Zeilen entstehen, die den Eindruck erweckt, größere Mengen wären teurer als kleinere, was das Vertrauen in die gesamte Preisdarstellung beschädigt.


query ProductTierPrices($sku: String!) {
  products(filter: { sku: { eq: $sku } }) {
    items {
      sku
      price_range {
        minimum_price {
          regular_price { value currency }
        }
      }
      price_tiers {
        quantity
        final_price { value currency }
        discount { percent_off }
      }
    }
  }
}

3. UI-Pattern für Mengenrabatte: Tabelle versus Stufen-Badges

Für B2B-lastige Kataloge mit vier oder mehr Staffelstufen hat sich eine kompakte Tabelle bewährt, die Menge, Stückpreis und Ersparnis nebeneinander zeigt, weil Einkäufer diese Zahlen häufig direkt für eigene Kalkulationen benötigen. Eine echte table-Struktur mit korrekt gesetzten scope-Attributen sorgt dabei zusätzlich für eine saubere Screenreader-Ausgabe, statt die Werte nur visuell über Flexbox oder Grid anzuordnen.

Für B2C-Shops mit meist nur zwei oder drei Staffeln wirkt dagegen ein einzelnes, prominent platziertes Badge wie ab 5 Stück minus zehn Prozent häufig überzeugender als eine vollständige Tabelle, weil es die Kaufentscheidung mit einer einzigen, leicht erfassbaren Information unterstützt, statt den Kunden mit einer Tabelle zu konfrontieren, die er gar nicht vollständig durchlesen möchte.


<table class="w-full text-sm border-collapse">
  <caption class="sr-only">Staffelpreise für dieses Produkt</caption>
  <thead>
    <tr class="border-b border-slate-200">
      <th scope="col" class="text-left py-2">Menge</th>
      <th scope="col" class="text-left py-2">Stückpreis</th>
      <th scope="col" class="text-left py-2">Ersparnis</th>
    </tr>
  </thead>
  <tbody>
    <template x-for="tier in sortedTiers" :key="tier.quantity">
      <tr class="border-b border-slate-100">
        <td class="py-2" x-text="`ab ${tier.quantity} Stk.`"></td>
        <td class="py-2" x-text="formatPrice(tier.final_price.value)"></td>
        <td class="py-2 text-green-700" x-text="`minus ${tier.discount.percent_off}%`"></td>
      </tr>
    </template>
  </tbody>
</table>

4. Sale-Countdown als eigenständige Alpine-Komponente

Ein Countdown-Timer lässt sich in Hyvä sauber als eigenständige, wiederverwendbare Alpine-Komponente kapseln, die einen Ziel-Zeitpunkt als Data-Attribut vom Server entgegennimmt und die verbleibende Zeit clientseitig in Sekunden herunterzählt. Wichtig ist, das Intervall in x-init zu starten und in einem passenden Cleanup wieder zu stoppen, damit beim Navigieren zwischen Alpine-Komponenten im selben DOM-Baum kein verwaister Timer im Hintergrund weiterläuft.

Der Ziel-Zeitpunkt sollte grundsätzlich als absoluter Unix-Timestamp übergeben werden, nicht als relative Restzeit wie zwei Tage und drei Stunden, denn eine relative Angabe wäre bereits im Moment des Renderns veraltet und würde bei jedem Full-Page-Cache-Treffer eine zunehmend falsche Restzeit anzeigen.


<div x-data="saleCountdown(<?= (int) $block->getData('sale_end_timestamp') ?>)" x-init="start()">
  <div class="flex gap-4 text-2xl font-bold" x-show="!expired">
    <div><span x-text="days"></span><span class="text-xs font-normal block">Tage</span></div>
    <div><span x-text="hours"></span><span class="text-xs font-normal block">Std.</span></div>
    <div><span x-text="minutes"></span><span class="text-xs font-normal block">Min.</span></div>
    <div><span x-text="seconds"></span><span class="text-xs font-normal block">Sek.</span></div>
  </div>
  <p x-show="expired" class="text-sm text-slate-500">Das Angebot ist abgelaufen.</p>
</div>

<script>
function saleCountdown(targetTimestamp) {
  return {
    days: 0, hours: 0, minutes: 0, seconds: 0, expired: false, timer: null,
    start() {
      this.tick();
      this.timer = setInterval(() => this.tick(), 1000);
    },
    tick() {
      const remaining = targetTimestamp - Math.floor(Date.now() / 1000);
      if (remaining <= 0) {
        this.expired = true;
        clearInterval(this.timer);
        return;
      }
      this.days = Math.floor(remaining / 86400);
      this.hours = Math.floor((remaining % 86400) / 3600);
      this.minutes = Math.floor((remaining % 3600) / 60);
      this.seconds = remaining % 60;
    },
  };
}
</script>

5. Server-Zeit-Synchronisation: warum der Client-Uhr nicht vertraut werden darf

Ein Countdown, der ausschließlich auf Date.now() im Browser basiert, vertraut implizit darauf, dass die Systemuhr des Kunden korrekt eingestellt ist. In der Praxis weichen Client-Uhren aus vielerlei Gründen ab, von falscher Zeitzone über nicht synchronisierte Systemuhren bis zu bewusst manipulierten Zeiteinstellungen, wodurch ein Kunde ein bereits abgelaufenes Angebot fälschlich noch als aktiv sieht oder umgekehrt ein noch laufendes Angebot als bereits beendet wahrnimmt.

Ein zuverlässigerer Ansatz gleicht die Client-Uhr einmalig beim Laden der Seite gegen die Serverzeit ab, etwa über den Date-Response-Header einer ohnehin ausgeführten Anfrage, und berechnet daraus einen Offset, der bei jeder weiteren Restzeit-Berechnung auf Date.now() angewendet wird. Der Countdown bleibt damit auch bei einer falsch eingestellten Client-Uhr korrekt, solange die Serverzeit selbst verlässlich ist.


// web/js/countdown/server-time-offset.js
export async function getServerTimeOffset() {
  const response = await fetch('/graphql', { method: 'HEAD' });
  const serverDate = new Date(response.headers.get('Date')).getTime();
  return serverDate - Date.now();
}

// Verwendung in der Alpine-Komponente
async function saleCountdown(targetTimestamp) {
  const offset = await getServerTimeOffset();
  return {
    tick() {
      const correctedNow = Math.floor((Date.now() + offset) / 1000);
      const remaining = targetTimestamp - correctedNow;
      // ... restliche Berechnung wie zuvor
    },
  };
}

6. Full Page Cache und Countdown-Anzeige: warum der Timer nicht eingefroren werden darf

Wird die verbleibende Restzeit bereits serverseitig als fertiger Text wie noch zwei Tage und drei Stunden gerendert, friert der Full Page Cache diese Angabe für die gesamte Cache-Lebensdauer der Seite ein, wodurch Kunden mit fortschreitender Zeit eine zunehmend falsche Restzeit sehen. Aus diesem Grund darf im gecachten HTML ausschließlich der feste Ziel-Zeitpunkt als Rohwert stehen, während die eigentliche Umrechnung in Tage, Stunden und Minuten komplett clientseitig erfolgt.

Dieses Prinzip, nur unveränderliche Rohdaten serverseitig zu rendern und jede zeitabhängige Darstellung dem Client zu überlassen, betrifft nicht nur den Countdown selbst, sondern auch angrenzende Elemente wie einen Hinweistext, der die verbleibende Zeit in Worten zusammenfasst. Auch dieser Text sollte aus dem Ziel-Zeitpunkt clientseitig generiert werden, statt als statischer, im Cache eingefrorener Satz im Markup zu stehen.

7. Sonderpreis-Kennzeichnung barrierefrei umsetzen

Ein durchgestrichener regulärer Preis neben einem hervorgehobenen Sonderpreis ist ein weit verbreitetes Muster, das aber nicht allein auf visuelle Mittel wie Farbe oder Durchstreichung setzen darf, um für Screenreader-Nutzer und Kunden mit eingeschränktem Farbsehen gleichermaßen verständlich zu sein. Das semantische s-Element für den durchgestrichenen Preis, kombiniert mit einem visuell verborgenen, aber für Screenreader vorhandenen Text, macht die Reduzierung für alle Nutzer gleichermaßen nachvollziehbar.

Ebenso wichtig ist eine textuelle Einordnung, die über eine reine Zahl hinausgeht, etwa statt bisher vor dem durchgestrichenen Preis, damit ohne visuellen Kontext klar wird, welcher der beiden Preise der aktuelle und welcher der frühere ist. Diese Details wirken auf den ersten Blick klein, entscheiden aber darüber, ob eine Preisreduzierung für assistive Technologien überhaupt als solche erkennbar ist.


<div class="flex items-baseline gap-2" aria-label="Sonderpreis 39,90 Euro statt regulär 59,90 Euro">
  <span class="text-2xl font-bold text-red-600">39,90 €</span>
  <span class="sr-only">statt bisher</span>
  <s class="text-slate-400 text-sm" aria-hidden="true">59,90 €</s>
  <span class="bg-red-100 text-red-700 text-xs font-semibold px-2 py-0.5 rounded">-33%</span>
</div>

8. Praxisbeispiel: Tier-Price, Countdown und Sonderpreis kombiniert auf der PDP

In einem realen PDP-Layout ordnen sich die drei Bausteine in fester Reihenfolge an: zuerst die Sonderpreis-Kennzeichnung mit durchgestrichenem regulärem Preis direkt unter dem Produktnamen, darunter der Countdown mit Ziel-Zeitpunkt aus dem Server-Zeit-Offset, und darunter die Staffelpreis-Tabelle für alle, die eine größere Menge in Erwägung ziehen. Jeder Baustein bleibt dabei als eigenständige Alpine-Komponente isoliert, sodass sich einzelne Teile unabhängig voneinander testen und bei Bedarf einzeln deaktivieren lassen.

Da alle drei Komponenten eigene script-Blöcke mitbringen, muss nach jedem einzelnen Inline-Skript $hyvaCsp->registerInlineScript() aufgerufen werden, damit die Content Security Policy die generierten Nonces korrekt zuordnet. Wird dieser Schritt bei einer der drei Komponenten vergessen, blockiert der Browser genau dieses eine Skript, während die übrigen zwei unauffällig weiterlaufen, was die Fehlersuche im Livebetrieb unnötig erschwert.

9. Checkliste für eine robuste Preisdarstellung im Hyvä-Theme

Preisdarstellung wirkt auf den ersten Blick wie reines Styling, entscheidet in der Praxis aber maßgeblich über Kundenvertrauen und Konversionsrate. Wer Staffelpreise, Countdown und Sonderpreis-Kennzeichnung als zusammenhängendes System betrachtet, statt jedes Element isoliert umzusetzen, vermeidet Widersprüche wie einen Countdown, der eine Restzeit von minus drei Stunden anzeigt, weil die Serverzeit-Synchronisation fehlte.

Die folgende Übersicht fasst die wichtigsten technischen und gestalterischen Anforderungen zusammen, die vor dem Go-Live eines neuen Preisdarstellungs-Patterns geprüft werden sollten.

Baustein Technische Anforderung Risiko bei Fehlern Priorität
Tier-Price-Sortierung Staffelstufen clientseitig nach Menge sortieren Verwirrende, scheinbar falsche Preistabelle Hoch
Countdown-Ziel-Zeitpunkt Absoluter Unix-Timestamp statt relativer Restzeit im Markup Falsche Restzeit durch Full Page Cache Sehr hoch
Server-Zeit-Synchronisation Offset gegen Date-Header statt reinem Date.now() Falsch angezeigter Countdown bei abweichender Client-Uhr Hoch
Sonderpreis-Kennzeichnung Semantisches s-Element plus versteckter Kontexttext Reduzierung für Screenreader-Nutzer nicht erkennbar Sehr hoch
CSP-Registrierung pro Komponente registerInlineScript nach jedem Inline-Skript-Block Einzelne Komponente wird von der CSP blockiert Hoch
Isolierte Alpine-Komponenten Tier-Price, Countdown und Sonderpreis getrennt kapseln Fehler in einer Komponente reißt die anderen mit Mittel

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

Preisdarstellung im Hyvä-Theme

Kernidee

Staffelpreise, Countdown und Sonderpreis müssen als zusammenhängendes, widerspruchsfreies System gedacht werden.

Countdown-Regel

Nur der absolute Ziel-Zeitpunkt gehört ins gecachte Markup, die Umrechnung erfolgt vollständig clientseitig.

Zeit-Vertrauen

Ein Offset gegen die Serverzeit verhindert falsche Countdown-Anzeigen bei abweichender Client-Uhr.

Barrierefreiheit

Sonderpreise dürfen sich nicht allein auf Farbe verlassen, sondern brauchen semantisches Markup und Kontexttext.

11. FAQ: Preisdarstellung im Hyvä-Theme

1Warum liefert Hyvä kein fertiges Preisdarstellungs-Pattern mit?
Weil sich die passende Darstellung stark nach Geschäftsmodell unterscheidet, ein B2B-Katalog mit vielen Staffelstufen braucht eine andere Lösung als ein B2C-Shop mit einem einzelnen Rabatt-Badge. Diese Entscheidung sollte bewusst und projektspezifisch getroffen werden, statt ein generisches Muster unreflektiert zu übernehmen.
2Warum müssen Staffelpreise vor der Anzeige sortiert werden?
Die GraphQL-API liefert die Reihenfolge der Staffelstufen nicht garantiert aufsteigend nach Menge. Ohne explizite Sortierung im Frontend kann eine Tabelle entstehen, die größere Mengen fälschlich als teurer erscheinen lässt.
3Warum sollte der Countdown-Ziel-Zeitpunkt als absoluter Timestamp übergeben werden?
Eine relative Angabe wie zwei Tage Restzeit wäre bereits im Moment des Renderns veraltet und würde durch den Full Page Cache dauerhaft eingefroren. Ein absoluter Unix-Timestamp bleibt dagegen unabhängig vom Cache-Zeitpunkt korrekt.
4Warum reicht Date.now() im Browser für einen zuverlässigen Countdown nicht aus?
Client-Uhren weichen aus vielen Gründen vom tatsächlichen Zeitpunkt ab, etwa durch falsche Zeitzonen oder nicht synchronisierte Systemuhren. Ein Offset gegen die Serverzeit gleicht diese Abweichung aus und hält den Countdown auch bei falsch gestellter Client-Uhr korrekt.
5Wie lässt sich die Serverzeit ohne zusätzlichen Endpunkt ermitteln?
Der Date-Response-Header einer ohnehin ausgeführten Anfrage, etwa an den GraphQL-Endpunkt, liefert die Serverzeit ohne zusätzliche Anfrage. Aus der Differenz zur lokalen Client-Zeit lässt sich ein Offset berechnen, der auf jede weitere Zeitberechnung angewendet wird.
6Warum darf die Restzeit nicht als fertiger Text serverseitig gerendert werden?
Ein serverseitig gerenderter Text wie noch zwei Tage Restzeit würde durch den Full Page Cache für die gesamte Cache-Lebensdauer eingefroren und mit fortschreitender Zeit zunehmend falsch. Nur der unveränderliche Ziel-Zeitpunkt darf im Cache liegen, die Umrechnung erfolgt clientseitig.
7Reicht eine Durchstreichung des regulären Preises für eine barrierefreie Sonderpreis-Kennzeichnung aus?
Nein, eine reine visuelle Durchstreichung ist für Screenreader-Nutzer unsichtbar. Ein semantisches s-Element kombiniert mit einem versteckten Kontexttext wie statt bisher macht die Reduzierung für alle Nutzer gleichermaßen erkennbar.
8Warum sollten Tier-Price, Countdown und Sonderpreis als getrennte Alpine-Komponenten umgesetzt werden?
Getrennte Komponenten lassen sich unabhängig voneinander testen, debuggen und bei Bedarf einzeln deaktivieren, ohne die übrigen Preisbausteine zu beeinflussen. Eine einzige, alles umfassende Komponente wird dagegen schnell unübersichtlich und schwer wartbar.
9Was passiert, wenn registerInlineScript für ein Inline-Skript vergessen wird?
Die Content Security Policy blockiert genau dieses eine Skript, während die übrigen Inline-Skripte auf derselben Seite weiterhin funktionieren. Das macht die Fehlersuche schwieriger, weil nur ein Teil der Seite betroffen ist und der Fehler nicht sofort offensichtlich wird.
10Wann lohnt sich eine Tabelle statt einzelner Rabatt-Badges für Staffelpreise?
Ab etwa vier Staffelstufen und bei einer eher B2B-geprägten Zielgruppe, die die Zahlen für eigene Kalkulationen nutzt, ist eine vollständige Tabelle meist verständlicher. Bei zwei bis drei Stufen in einem B2C-Kontext wirkt ein einzelnes, prominentes Badge dagegen oft überzeugender.