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.
Inhaltsverzeichnis
- 1. Warum Preisdarstellung im Hyvä-Theme mehr ist als eine einzelne Zahl
- 2. Tier-Price-Daten aus GraphQL abrufen und korrekt aufbereiten
- 3. UI-Pattern für Mengenrabatte: Tabelle versus Stufen-Badges
- 4. Sale-Countdown als eigenständige Alpine-Komponente
- 5. Server-Zeit-Synchronisation: warum der Client-Uhr nicht vertraut werden darf
- 6. Full Page Cache und Countdown-Anzeige: warum der Timer nicht eingefroren werden darf
- 7. Sonderpreis-Kennzeichnung barrierefrei umsetzen
- 8. Praxisbeispiel: Tier-Price, Countdown und Sonderpreis kombiniert auf der PDP
- 9. Checkliste für eine robuste Preisdarstellung im Hyvä-Theme
- 10. Zusammenfassung
- 11. FAQ
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.