Hyvä, Alpine.js und die richtige Redirect-Logik
Ein Magento 2 Store Switcher wirkt im Frontend wie ein simples Dropdown, verbirgt dahinter aber eine Redirect-Logik, die entscheidet, ob ein Kunde beim Sprachwechsel auf derselben Produktseite landet oder unerwartet zur Startseite springt. Wer den Store Switcher in Hyvä mit Alpine.js sauber anpasst, erhält Seitenkontext, respektiert Cookie-Präferenzen und vermeidet Frust bei internationalen Besuchern.
Inhaltsverzeichnis
- 1. Warum der Store Switcher mehr als ein Dropdown ist
- 2. Der native Store Switcher Block und sein Datenmodell
- 3. Store Switcher in Hyvä mit Alpine.js umsetzen
- 4. Redirect-Logik: URL in Store versus Store-Code-Cookie
- 5. Seitenkontext beim Wechsel erhalten
- 6. Cookie-Persistenz und wiederkehrende Besucher
- 7. Store-Sichtbarkeit nach Kundengruppe oder GeoIP steuern
- 8. SEO-Auswirkungen: hreflang und Store Switcher zusammen denken
- 9. Implementierungsvarianten im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum der Store Switcher mehr als ein Dropdown ist
Für den Kunden ist der Magento 2 Store Switcher ein einfaches Element in der Kopfzeile, meist als Dropdown mit Länder- oder Sprachfahnen dargestellt. Technisch steckt dahinter jedoch ein Zusammenspiel aus Store-Resolution, URL-Struktur, Redirect-Handling und optional Cookie-Persistenz, das bei unsauberer Implementierung zu genau den Frustmomenten führt, die ein internationaler Shop eigentlich vermeiden will: Der Kunde wechselt die Sprache und landet plötzlich auf der Startseite statt auf der gerade betrachteten Produktseite.
Diese Komplexität wird oft unterschätzt, weil der Standard-Store-Switcher in einer einfachen Single-Language-Installation tatsächlich unauffällig funktioniert. Sobald jedoch mehrere Store Views mit unterschiedlichen URL-Strategien, etwa Store-Code in der URL versus separate Domains, kombiniert werden, muss der Magento 2 Store Switcher gezielt angepasst werden, um konsistent zu funktionieren. Die folgenden Abschnitte zeigen, wie der native Mechanismus arbeitet und wie er sich in Hyvä mit Alpine.js robust erweitern lässt.
2. Der native Store Switcher Block und sein Datenmodell
Der native Store Switcher basiert auf Magento\Store\Block\Switcher, der über getStoreSwitcherOptions() alle sichtbaren Stores der aktuellen Website oder Store Group liefert. Für jede Option wird eine URL berechnet, die auf dieselbe Seite im Ziel-Store zeigt, sofern der Store-URL-Rewrite dafür existiert. Existiert kein passendes Rewrite, weil etwa ein Produkt in diesem Store nicht sichtbar ist, verweist die berechnete URL stattdessen auf die Startseite des Ziel-Stores.
Ein zentraler Baustein ist der Query-Parameter ___store, den Magento an die berechnete URL anhängt. Dieser Parameter wird von Magento\Store\App\Response\Redirect ausgewertet und löst einen serverseitigen Redirect mit gesetztem Store-Cookie aus, bevor die eigentliche Zielseite ausgeliefert wird. Wer den Magento 2 Store Switcher individuell anpasst, sollte diesen Mechanismus nicht umgehen, sondern gezielt erweitern, da er bereits die korrekte Store-Initialisierung für den gesamten nachfolgenden Request übernimmt.
<?php
declare(strict_types=1);
namespace Mironsoft\StoreSwitcher\ViewModel;
use Magento\Framework\View\Element\Block\ArgumentInterface;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Store\Api\Data\StoreInterface;
/**
* ViewModel providing switcher options filtered by an allowed store list.
*/
final class SwitcherOptions implements ArgumentInterface
{
/**
* @param StoreManagerInterface $storeManager Resolves all active stores
*/
public function __construct(
private readonly StoreManagerInterface $storeManager
) {
}
/**
* Return visible stores excluding staging/internal store views.
*
* @return StoreInterface[] Store views eligible for the switcher
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getVisibleStores(): array
{
$stores = $this->storeManager->getStores();
return array_filter(
$stores,
static fn (StoreInterface $store): bool => !str_starts_with(
(string) $store->getCode(),
'internal_'
)
);
}
}
3. Store Switcher in Hyvä mit Alpine.js umsetzen
In Hyvä ersetzt ein leichtgewichtiges phtml-Template mit Alpine.js die schwergewichtige Knockout.js-Implementierung von Luma. Der Magento 2 Store Switcher lässt sich dabei als einfache x-data-Komponente umsetzen, die den Dropdown-Zustand hält und beim Klick auf eine Option direkt zur berechneten Store-URL navigiert. Wichtig ist, dass die im Block bereits berechneten URLs samt ___store-Parameter unverändert übernommen werden, damit der native Redirect-Mechanismus weiterhin greift.
Für die visuelle Gestaltung genügt in Hyvä eine Tailwind-basierte Dropdown-Struktur ohne zusätzliches JavaScript-Bundle, da Alpine.js bereits Teil des Themes ist. Der Vorteil gegenüber der Luma-Variante liegt nicht nur in der Ladezeit, sondern auch in der Übersichtlichkeit: Die gesamte Logik des Store Switchers ist in einer einzigen phtml-Datei mit wenigen Zeilen Alpine.js-Markup sichtbar, statt über mehrere Knockout-Komponenten verteilt zu sein.
<?php
/** @var \Magento\Store\Block\Switcher $block */
/** @var \Magento\Framework\Escaper $escaper */
$switcherViewModel = $block->getData('viewModel');
?>
<div x-data="{ open: false }" class="relative" @click.outside="open = false">
<button
@click="open = !open"
class="flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900"
aria-haspopup="listbox"
:aria-expanded="open.toString()"
>
<?= $escaper->escapeHtml($block->getCurrentStoreName()) ?>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</button>
<ul
x-show="open"
x-transition
x-cloak
class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-lg shadow-lg z-20"
role="listbox"
>
<?php foreach ($block->getStoreSwitcherOptions() as $store): ?>
<li>
<a href="<?= $escaper->escapeUrl($store->getUrl()) ?>"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
data-store-code="<?= $escaper->escapeHtmlAttr($store->getCode()) ?>">
<?= $escaper->escapeHtml($store->getName()) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
4. Redirect-Logik: URL in Store versus Store-Code-Cookie
Magento unterstützt zwei grundsätzliche URL-Strategien für Store Views, gesteuert über web/url/use_store. Ist diese Option aktiviert, erscheint der Store-Code als Teil des Pfads, etwa /de/produkt.html, was den Store Switcher technisch einfacher macht, da die URL selbst bereits eindeutig einem Store zugeordnet ist. Ist die Option deaktiviert, was der Standard und für produktive Shops empfohlen ist, sind die URLs pro Store view identisch, und Magento unterscheidet ausschließlich über Domain, Cookie oder den ___store-Parameter beim initialen Request.
Die deaktivierte Variante mit separaten Domains pro Store ist für SEO deutlich vorteilhafter, da sie doppelten Content über identische Pfade vermeidet, erfordert aber eine sorgfältigere Store Switcher-Implementierung: Der Switcher muss beim Wechsel aktiv zur richtigen Domain samt Store-Parameter verlinken, statt sich auf eine automatische Pfad-Erkennung zu verlassen. Ein häufiger Fehler ist, den Store Switcher clientseitig per JavaScript umzuschreiben, ohne den serverseitigen Redirect-Mechanismus zu durchlaufen, wodurch das Store-Cookie nicht korrekt gesetzt wird und der nächste Seitenaufruf wieder im alten Store landet.
#!/usr/bin/env bash
set -euo pipefail
# Verify the native ___store redirect actually sets the store cookie
curl -sI "https://shop.example.com/product.html?___store=at" \
| grep -iE 'location|set-cookie'
# Expected: a 302/301 redirect to the clean URL plus
# Set-Cookie: store=at; ...
# If Set-Cookie is missing, the redirect mechanism was bypassed somewhere
# (e.g. a client-side rewrite instead of following the native link)
5. Seitenkontext beim Wechsel erhalten
Eine der größten Erwartungen an einen gut gebauten Store Switcher ist, dass der Kunde nach dem Sprachwechsel auf derselben inhaltlichen Seite landet, etwa demselben Produkt in der neuen Sprache, statt auf der Startseite. Magento löst das grundsätzlich über URL-Rewrites: Existiert für die aktuelle Entity, etwa ein Produkt oder eine Kategorie, ein Rewrite im Ziel-Store, berechnet getStoreSwitcherOptions() automatisch die passende URL für diesen Store.
Problematisch wird es, wenn ein Produkt im Ziel-Store nicht zugewiesen oder dort nicht sichtbar ist. In diesem Fall fällt Magento auf die Store-Startseite zurück, was für den Kunden wie ein Fehler wirkt, auch wenn es technisch korrektes Verhalten ist. Eine sinnvolle Erweiterung des Store Switchers ist daher, dem Kunden in diesem Fall eine kurze Meldung anzuzeigen, etwa "Dieses Produkt ist im gewählten Store nicht verfügbar, hier finden Sie ähnliche Artikel", statt ihn kommentarlos auf der Startseite zu belassen.
6. Cookie-Persistenz und wiederkehrende Besucher
Für wiederkehrende Besucher ist es sinnvoll, die einmal getroffene Store-Entscheidung über den Besuch hinaus zu speichern, damit der Store Switcher nicht bei jedem neuen Besuch erneut bedient werden muss. Magento setzt dafür automatisch das Cookie store beim Store-Wechsel über den ___store-Parameter, dessen Lebensdauer über web/cookie/cookie_lifetime konfiguriert wird. Bei einem erneuten Besuch ohne expliziten Store-Parameter in der URL greift Magento auf dieses Cookie zurück, sofern kein anderer Mechanismus, etwa Domain-basiertes Routing, Vorrang hat.
Für DSGVO-konforme Umsetzungen ist zu beachten, dass dieses Cookie funktional notwendig ist und in der Regel ohne explizite Einwilligung gesetzt werden darf, solange es ausschließlich der technischen Store-Zuordnung dient und keine Tracking-Zwecke verfolgt. Wird der Store Switcher um zusätzliche Präferenzen erweitert, etwa eine gemerkte Wunschwährung unabhängig vom Store, sollte diese Erweiterung im Cookie-Consent-Banner separat aufgeführt werden, um rechtlich auf der sicheren Seite zu bleiben.
7. Store-Sichtbarkeit nach Kundengruppe oder GeoIP steuern
Nicht jeder Store view soll für jeden Besucher im Store Switcher sichtbar sein. Ein B2B-Store view etwa soll unter Umständen nur für eingeloggte Firmenkunden erscheinen, während ein Store view für ein noch nicht offiziell gestartetes Land ausschließlich für interne Testzwecke sichtbar bleiben soll. Der native Block liefert diese Filterlogik nicht mit, weshalb ein Plugin auf getStoreSwitcherOptions() nötig ist, das die Liste anhand von Kundengruppe, GeoIP-Erkennung oder einem eigenen Freigabe-Flag pro Store view filtert.
GeoIP-basierte Filterung ist besonders vorsichtig zu implementieren, da sie niemals eine Store-Auswahl vollständig verbergen sollte, die ein Kunde bewusst über einen direkten Link erreicht hat. Der Store Switcher darf die Liste sichtbarer Optionen einschränken, aber der direkte Zugriff über eine explizite Store-URL sollte unabhängig von der GeoIP-Erkennung weiterhin funktionieren, da sonst Support-Anfragen von Kunden entstehen, die über einen geteilten Link kommen und plötzlich nichts mehr sehen.
<?php
declare(strict_types=1);
namespace Mironsoft\StoreSwitcher\Plugin;
use Magento\Store\Block\Switcher;
use Magento\Customer\Model\Session as CustomerSession;
/**
* Filters the visible store switcher options by customer group,
* hiding B2B-only store views from guests and retail customers.
*/
final class FilterSwitcherByCustomerGroup
{
private const int B2B_STORE_ID = 5;
private const int B2B_CUSTOMER_GROUP_ID = 3;
/**
* @param CustomerSession $customerSession Current customer session
*/
public function __construct(
private readonly CustomerSession $customerSession
) {
}
/**
* @param Switcher $subject Native store switcher block
* @param array $result Native list of switcher options
* @return array Filtered list, excluding B2B store views for non-B2B customers
*/
public function afterGetStoreSwitcherOptions(Switcher $subject, array $result): array
{
if ($this->customerSession->getCustomerGroupId() === self::B2B_CUSTOMER_GROUP_ID) {
return $result;
}
return array_filter(
$result,
static fn ($store): bool => (int) $store->getId() !== self::B2B_STORE_ID
);
}
}
8. SEO-Auswirkungen: hreflang und Store Switcher zusammen denken
Der Store Switcher und die hreflang-Verlinkung im Head-Bereich einer Seite sollten immer dieselbe Datenquelle nutzen. Ein häufiger, schwer zu findender Fehler entsteht, wenn beide Elemente unabhängig voneinander implementiert werden und dadurch unterschiedliche Store-URLs berechnen, etwa weil der Store Switcher clientseitig eine andere Logik verwendet als das serverseitig gerenderte hreflang-Tag. Suchmaschinen werten solche Inkonsistenzen als Signal für unzuverlässige internationale Zielgruppen-Signale.
Eine saubere Architektur zieht die Store-URL-Berechnung in eine zentrale ViewModel-Klasse, die sowohl vom hreflang-Block im Head als auch vom Store Switcher im Header konsumiert wird. Diese Vereinheitlichung stellt sicher, dass beide Elemente immer dieselben Ziel-URLs referenzieren, unabhängig davon, an welcher Stelle im Layout sie gerendert werden, und reduziert das Risiko widersprüchlicher internationaler Signale erheblich.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Store\Block\Switcher">
<plugin name="mironsoft_filterSwitcherByCustomerGroup"
type="Mironsoft\StoreSwitcher\Plugin\FilterSwitcherByCustomerGroup"
sortOrder="10" />
</type>
</config>
9. Implementierungsvarianten im Vergleich
Die folgende Übersicht vergleicht die drei gängigsten Implementierungsvarianten für den Magento 2 Store Switcher.
| Variante | Seitenkontext erhalten | SEO-Eignung | Aufwand |
|---|---|---|---|
| Nativer Block, unverändert | Nur bei existierendem Rewrite | Solide, kein hreflang-Abgleich | Sehr gering |
| Hyvä + Alpine.js, native URLs | Ja, identisch zu nativ | Gut, leicht erweiterbar | Gering bis mittel |
| Zentrales ViewModel für Switcher + hreflang | Ja, mit Fallback-Meldung | Optimal, konsistente Signale | Höher, aber einmalig |
Für internationale Shops mit mehr als zwei bis drei Store Views lohnt sich die dritte Variante fast immer, da die einmalige Investition in ein zentrales ViewModel spätere Inkonsistenzen zwischen Store Switcher und SEO-Tags von Anfang an ausschließt.
Mironsoft
Magento 2 Multi Store und Internationalisierung
Store Switcher, der Seitenkontext und SEO respektiert?
Wir bauen Store Switcher in Hyvä mit Alpine.js, koppeln sie sauber an hreflang-Tags und stellen sicher, dass Kunden beim Wechsel auf der richtigen Seite landen, nicht auf der Startseite.
Hyvä-Umsetzung
Alpine.js-basierter Store Switcher ohne zusätzliches JS-Bundle
SEO-Konsistenz
Zentrales ViewModel für Switcher und hreflang-Tags
Sichtbarkeits-Regeln
Store-Filterung nach Kundengruppe oder Freigabe-Flag
10. Zusammenfassung
Ein robuster Magento 2 Store Switcher nutzt den nativen ___store-Redirect-Mechanismus, statt ihn per clientseitigem JavaScript zu umgehen, und erhält damit korrekte Cookie-Zustände für die gesamte Session. In Hyvä lässt sich der Switcher elegant mit Alpine.js umsetzen, ohne die berechneten Store-URLs des nativen Blocks zu verändern. Seitenkontext bleibt erhalten, solange ein passendes URL-Rewrite im Ziel-Store existiert, andernfalls sollte der Kunde eine erklärende Meldung statt einer stillen Weiterleitung zur Startseite sehen.
Store-Sichtbarkeit nach Kundengruppe oder GeoIP erfordert ein eigenes Plugin, sollte aber niemals den direkten Zugriff über eine explizite Store-URL blockieren. Die größte Qualitätssteigerung entsteht, wenn Store Switcher und hreflang-Tags dieselbe zentrale URL-Berechnung nutzen, statt unabhängig voneinander gepflegt zu werden.
Magento 2 Store Switcher — Das Wichtigste auf einen Blick
___store-Parameter respektieren
Native Store-URLs mit ___store-Parameter unverändert übernehmen, damit Redirect und Cookie-Setzung korrekt greifen.
Alpine.js statt Knockout.js
In Hyvä genügt eine schlanke x-data-Komponente ohne zusätzliches JavaScript-Bundle.
Seitenkontext-Fallback
Fehlt ein Rewrite im Ziel-Store, erklärende Meldung statt stiller Startseiten-Weiterleitung anzeigen.
Konsistenz mit hreflang
Store Switcher und hreflang-Tags über dieselbe ViewModel-Logik berechnen, um widersprüchliche SEO-Signale zu vermeiden.