Sortierung, Grid/List-Umschalter und Pagination sauber implementieren
Wer die Kategorieseiten-Toolbar im Hyvä-Theme anpasst, arbeitet mit einem einzigen Block, einem einzigen Template und Alpine.js statt mit Knockout-Bindings und UI-Components-Grids. Diese Anleitung zeigt, wie Sortierfelder erweitert, ein View-Mode-Umschalter mit Persistenz gebaut und die Pagination der Hyvä-Kategorieseite ohne Bruch mit dem Full Page Cache angepasst werden.
Inhaltsverzeichnis
- 1. Wie die Kategorieseite im Hyvä-Theme aufgebaut ist
- 2. Layout-Architektur: category.xml, product_list_toolbar.xml und Container-Hierarchie
- 3. Sortierung anpassen: Sortierfelder, EAV-Attribute und ViewModel
- 4. Alpine.js Sortier-Dropdown statt nativer select
- 5. Grid/List-Umschalter mit Persistenz
- 6. Pagination anpassen: Items-per-page und eigenes Pager-Design
- 7. Mobile-Toolbar: Sticky-Header und Bottom-Sheet
- 8. GraphQL/Headless-Aspekte der Kategorieseiten-Toolbar
- 9. Performance und Full-Page-Cache-Kompatibilität
- 10. Zusammenfassung
- 11. FAQ
1. Wie die Kategorieseite im Hyvä-Theme aufgebaut ist
Die Hyvä-Kategorieseite rendert ihre Toolbar über denselben Block wie Luma: Magento\Catalog\Block\Product\ProductList\Toolbar. Der entscheidende Unterschied liegt nicht in der PHP-Schicht, sondern im Template. Statt Magento_Catalog::product/list/toolbar.phtml mit Knockout-Bindings, separaten Wissen-Templates wie sorter.html, limiter.html und toolbar-amount.html sowie einem per RequireJS nachgeladenen UI-Components-Grid liefert Hyvä ein einziges, serverseitig vollständig gerendertes phtml-Template aus. Es gibt kein Client-Side-Rendering der Toolbar, keine data-bind-Attribute und keine asynchrone Nachladung von Sortier- oder Limiter-Widgets.
Der Toolbar-Block stellt die bekannten Getter bereit: getCurrentOrder(), getCurrentDirection(), getAvailableOrders(), getCurrentMode(), getModes(), getLimit() und getAvailableLimit(). Im Hyvä-Theme hyva-themes/magento2-default-theme-csp liegt die Template-Überschreibung unter Magento_Catalog/templates/product/list/toolbar.phtml und nutzt diese Getter direkt in Tailwind-Markup, ergänzt um Alpine.js-Direktiven für die interaktiven Teile. Das reduziert die Zahl der beteiligten Dateien drastisch: Ein phtml, ein optionales ViewModel, ein Alpine-Komponentenskript, fertig.
Wichtig für die Abgrenzung: Die Kategorieseiten-Toolbar behandelt ausschließlich Sortierung, Ansichtsmodus und Seitennavigation. Die Sidebar mit Facetten wie Farbe, Größe oder Preis-Slider ist ein komplett separater Block (Magento_LayeredNavigation) mit eigenem Rendering-Pfad und wird hier bewusst nicht behandelt. Wer beide Bereiche vermischt, verliert schnell den Überblick über Zuständigkeiten im Layout.
2. Layout-Architektur: category.xml, product_list_toolbar.xml und Container-Hierarchie
Die Layout-Definition der Hyvä-Kategorieseite beginnt bei catalog_category_view.xml. Dort referenziert der Block category.products (Klasse Magento\Catalog\Block\Product\ListProduct) über das Argument toolbar_block_name den Kindblock product_list_toolbar. Dieser Block wird in der Standard-Layout-Kette zweimal in die Ausgabe eingebunden: einmal oberhalb und einmal unterhalb der Produktliste. Beide Instanzen teilen sich Zustand und Template, unterscheiden sich aber im Rendering-Kontext, was für eigene Anpassungen der Kategorieseiten-Toolbar relevant ist, wenn oben und unten unterschiedliche Elemente sichtbar sein sollen.
Um die Toolbar im eigenen Theme zu überschreiben, legt man in app/design/frontend/Mironsoft/default/Magento_Catalog/layout/catalog_category_view.xml ein referenceBlock name="product_list_toolbar" an und setzt dort ein eigenes Template oder ein zusätzliches ViewModel-Argument. Die Container-Hierarchie folgt dabei category.view.container → category.products → product_list_toolbar, wobei category.products selbst wiederum die eigentliche Produktliste als Geschwisterblock einbindet. Diese Struktur bleibt im Hyvä-Theme unverändert, nur die Templates darunter werden ersetzt.
Ein häufiger Fehler bei der Überschreibung: Entwickler legen ein komplett neues product_list_toolbar.phtml an, ohne die vom Block bereitgestellten Getter zu respektieren, und brechen dadurch die Synchronisation zwischen den beiden Toolbar-Instanzen oben und unten. Sauberer ist es, das ViewModel-Argument im Layout-XML zu erweitern und im Template nur zusätzliche Markup-Blöcke zu ergänzen, statt die komplette Logik neu zu schreiben.
| Aufgabe | Naiver Luma-Ansatz | Empfohlenes Hyvä-Pattern | Vorteil |
|---|---|---|---|
| Sortier-Dropdown | Knockout-Select mit data-bind | Alpine x-data Custom-Dropdown | Kein Knockout-Overhead, volle Tastaturbedienung |
| View-Mode-Persistenz | Session-Variable serverseitig | Alpine + localStorage/Cookie | Kein Zusatzrequest, FPC-neutral |
| Pagination | UI-Components-Pager mit RequireJS | Serverseitiges phtml mit query-Parametern | Direkt indexierbar, kein JS-Grid nötig |
| Mobile-Toolbar | Fixiertes Overlay ohne Transition | Alpine x-show/x-transition Bottom-Sheet | Flüssige Animation, kleine Bundle-Größe |
| FPC-Kompatibilität | Serverseitige Sortier-Session | URL-Parameter + clientseitiger View-Mode | Jede Sortier-URL bleibt cachebar |
3. Sortierung anpassen: Sortierfelder, EAV-Attribute und ViewModel
Die Standard-Sortierfelder der Hyvä-Kategorieseite sind Position, Preis, Name und, sofern konfiguriert, Neuheit über created_at. Welche Felder tatsächlich angeboten werden, steuert das EAV-Attribut-Flag used_for_sort_by. Ein neues Sortierfeld, etwa Bewertungsdurchschnitt oder ein individuelles Feature-Attribut, wird über eine Data-Patch-Klasse (Magento\Framework\Setup\Patch\DataPatchInterface) gesetzt, die das Attribut lädt und used_for_sort_by auf 1 setzt, statt ein veraltetes InstallScript zu verwenden.
Für die Darstellung der verfügbaren Sortieroptionen empfiehlt sich ein eigenes ViewModel statt einer Block-Klasse. Ein SortOptionsViewModel, das ArgumentInterface implementiert, kapselt die Logik, welche Sortierfelder in welcher Reihenfolge und mit welchem Label angezeigt werden, und lässt sich per Constructor Property Promotion mit dem Toolbar-Block, dem Attribut-Repository und dem Store-Manager verdrahten. Das Template ruft dann nur noch $sortOptionsViewModel->getAvailableOrders() auf, ohne Businesslogik im phtml zu duplizieren.
Ein Detail, das in vielen Kategorieseiten-Toolbar-Implementierungen übersehen wird: Die Sortierrichtung (asc/desc) ist pro Feld unterschiedlich sinnvoll vorbelegt. Preis startet meist aufsteigend, Neuheit absteigend. Das ViewModel sollte diese Standardrichtung je Feld zurückgeben, statt im Template mit Bedingungen zu arbeiten.
<?php
declare(strict_types=1);
namespace Mironsoft\CategoryToolbar\ViewModel;
use Magento\Catalog\Model\Product\ProductList\Toolbar;
use Magento\Eav\Api\AttributeRepositoryInterface;
use Magento\Framework\View\Element\Block\ArgumentInterface;
/**
* Provides sort options for the Hyva category page toolbar.
*/
final class SortOptionsViewModel implements ArgumentInterface
{
/**
* @param Toolbar $toolbar Catalog toolbar model providing current sort state.
* @param AttributeRepositoryInterface $attributeRepository Repository used to resolve EAV attribute labels.
*/
public function __construct(
private readonly Toolbar $toolbar,
private readonly AttributeRepositoryInterface $attributeRepository,
) {
}
/**
* Returns available sort fields with label and default direction.
*
* @return array<string, array{label: string, dir: string}>
*/
public function getAvailableOrders(): array
{
$orders = [];
foreach ($this->toolbar->getAvailableOrders() as $code => $label) {
$orders[$code] = [
'label' => (string) $label,
'dir' => $this->getDefaultDirection($code),
];
}
return $orders;
}
/**
* Returns the currently active sort field code.
*
* @return string
*/
public function getCurrentOrder(): string
{
return (string) $this->toolbar->getCurrentOrder();
}
/**
* Resolves a sensible default direction per sort field.
*
* @param string $code Sort field code, e.g. "price" or "created_at".
* @return string Either "asc" or "desc".
*/
private function getDefaultDirection(string $code): string
{
return match ($code) {
'created_at' => 'desc',
default => 'asc',
};
}
}
4. Alpine.js Sortier-Dropdown statt nativer select
Ein nativer <select> ist barrierefrei und funktioniert, lässt sich aber optisch kaum an ein Tailwind-Design anpassen, ohne Browser-Inkonsistenzen zu riskieren. Für die Hyvä-Kategorieseite ist deshalb ein Alpine-basiertes Dropdown das gängige Muster: eine x-data-Komponente hält den geöffneten Zustand, das aktuell gewählte Sortierfeld und die Richtung, während ein <button> mit role="listbox"-Semantik und <ul> mit role="option"-Einträgen die eigentliche Darstellung übernimmt.
Tastaturbedienbarkeit ist dabei kein optionales Extra, sondern Voraussetzung. Pfeiltasten navigieren zwischen den Optionen, Enter bestätigt, Escape schließt das Dropdown und gibt den Fokus an den auslösenden Button zurück. Alpine bildet das über @keydown.arrow-down.prevent, @keydown.arrow-up.prevent, @keydown.enter und @keydown.escape ab, kombiniert mit x-ref für den programmatischen Fokuswechsel. Wichtig ist, niemals literale Mustache-Syntax zu verwenden, sondern konsequent x-text für dynamische Textinhalte einzusetzen.
Beim Klick auf eine Option navigiert Alpine nicht per Fetch, sondern setzt window.location auf die vom Server bereits vorbereitete Sortier-URL mit dem passenden product_list_order- und product_list_dir-Parameter. Das ist bewusst so gestaltet: Ein vollständiger Seitenaufruf mit neuem Query-String bleibt für den Full Page Cache identifizierbar, während ein reines Client-Side-Update der Produktliste zusätzliche GraphQL- oder REST-Aufrufe erfordern würde.
/**
* Alpine.js component for the Hyva category page toolbar sort dropdown.
* Handles keyboard navigation and full-page navigation on selection.
*/
function categorySortDropdown(currentOrder, currentDir) {
return {
open: false,
currentOrder,
currentDir,
highlightedIndex: 0,
/**
* Toggles the dropdown and resets keyboard highlight.
*/
toggle() {
this.open = !this.open;
if (this.open) {
this.highlightedIndex = 0;
this.$nextTick(() => this.$refs.list.focus());
}
},
/**
* Moves the keyboard highlight within the option list.
* @param {number} delta -1 for up, 1 for down.
* @param {number} total Total number of options.
*/
move(delta, total) {
this.highlightedIndex = (this.highlightedIndex + delta + total) % total;
},
/**
* Applies the chosen sort field and triggers a full page navigation.
* @param {string} code Sort field code, e.g. "price".
* @param {string} dir Sort direction, "asc" or "desc".
* @param {string} url Pre-built target URL from the server.
*/
select(code, dir, url) {
this.currentOrder = code;
this.currentDir = dir;
this.open = false;
window.location.assign(url);
},
/**
* Closes the dropdown and returns focus to the trigger button.
*/
close() {
this.open = false;
this.$refs.trigger.focus();
},
};
}
5. Grid/List-Umschalter mit Persistenz
Der View-Mode-Toggle der Hyvä-Kategorieseite wechselt zwischen einer Grid-Darstellung mit mehreren Spalten und einer List-Darstellung mit einer Spalte und ausführlicherem Produkttext. Anders als bei der Sortierung, die eine neue serverseitige Antwort mit anderer Produktreihenfolge erfordert, ist der Ansichtsmodus rein präsentational: Dieselben Produktdaten werden nur anders angeordnet. Genau deshalb gehört diese Funktion konsequent auf die Client-Seite.
Das übliche Muster kombiniert eine Alpine-Komponente mit localStorage für die Persistenz über Seitenaufrufe hinweg und optional einem ViewModel, das den initial gerenderten Modus serverseitig aus einem Cookie liest, um einen Layout-Sprung (Flash of Wrong Layout) beim ersten Rendern zu vermeiden. Das phtml rendert dazu beide Layout-Varianten mit bedingten Tailwind-Klassen wie grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 für die Grid-Ansicht und flex flex-col divide-y für die List-Ansicht, wobei Alpine per :class zwischen beiden umschaltet, ohne dass ein neuer Request an den Server nötig wird.
Für produktionsreife Kategorieseiten-Toolbar-Implementierungen empfiehlt sich zusätzlich ein serverseitiges Cookie über Magento\Framework\Stdlib\CookieManagerInterface, das beim ersten Request gelesen wird und die Standard-CSS-Klasse im initial ausgelieferten HTML festlegt. So bleibt der Seitenaufbau auch mit deaktiviertem JavaScript in der zuletzt gewählten Ansicht konsistent, während Alpine im Browser nur noch die Umschaltung ohne Neuladen übernimmt.
/**
* Alpine.js component for the Hyva category page grid/list view-mode toggle.
* Persists the choice in localStorage and syncs an outgoing cookie.
*/
function categoryViewMode(initialMode) {
return {
mode: initialMode,
/**
* Initializes the component and restores a previously saved mode.
*/
init() {
const saved = window.localStorage.getItem('category_view_mode');
if (saved === 'grid' || saved === 'list') {
this.mode = saved;
}
},
/**
* Switches the view mode and persists it for future visits.
* @param {string} mode Either "grid" or "list".
*/
setMode(mode) {
this.mode = mode;
window.localStorage.setItem('category_view_mode', mode);
document.cookie = 'category_view_mode=' + mode + '; path=/; max-age=31536000; samesite=lax';
},
/**
* Returns whether a given mode is currently active.
* @param {string} mode Mode identifier to compare.
* @returns {boolean}
*/
isActive(mode) {
return this.mode === mode;
},
};
}
6. Pagination anpassen: Items-per-page und eigenes Pager-Design
Die Pagination der Hyvä-Kategorieseite besteht aus zwei unabhängigen Steuerelementen: dem Items-per-page-Selector (Limiter), der über getAvailableLimit() die konfigurierten Werte aus Stores > Configuration > Catalog > Storefront bezieht, und dem eigentlichen Pager mit Seitenzahlen, Vor- und Zurück-Links. Beide werden im Standard-Hyvä-Template als einfache Links mit vollständigem Query-String gerendert, nicht als per JavaScript nachgeladenes Grid.
Ein eigenes Pager-Design ersetzt üblicherweise die Standard-Darstellung durch eine kompaktere Variante mit maximal fünf sichtbaren Seitenzahlen plus Ellipsis-Platzhaltern, wenn die Gesamtzahl der Seiten größer ist. Die Berechnung, welche Seitenzahlen angezeigt werden, gehört in ein ViewModel oder in eine dedizierte Pager-Klasse, nicht in das Template selbst, damit die Logik testbar bleibt und nicht bei jeder Design-Anpassung neu geschrieben werden muss.
Da die Toolbar in der Standard-Layout-Struktur zweimal eingebunden ist, oberhalb und unterhalb der Produktliste, bietet es sich an, oben die vollständige Toolbar mit Sortierung, View-Mode und Limiter zu zeigen und unten nur den Pager, um Redundanz auf langen Kategorieseiten zu vermeiden. Diese Unterscheidung lässt sich über ein Block-Argument im Layout-XML steuern, das dem Template mitteilt, welche Sektionen es rendern soll.
7. Mobile-Toolbar: Sticky-Header und Bottom-Sheet
Auf kleinen Bildschirmen verdrängt eine vollständige Toolbar mit Sortierung, View-Mode und Limiter schnell den sichtbaren Produktbereich. Das etablierte Muster für die mobile Hyvä-Kategorieseite reduziert die sichtbare Toolbar auf einen kompakten Button-Streifen mit Sortier- und Filter-Trigger, während die eigentlichen Sortieroptionen in einem Bottom-Sheet ausgelagert werden, das per Alpine x-show und x-transition von unten einfährt.
Damit die Toolbar beim Scrollen durch lange Produktlisten erreichbar bleibt, wird der Button-Streifen mit sticky top-0 z-10 plus einem dezenten backdrop-blur und Hintergrundfarbe fixiert. Wichtig ist ein ausreichender z-index, der über dem Produktgrid, aber unter modalen Overlays liegt, sowie ein shadow-sm, sobald die Seite gescrollt wurde, um die Toolbar optisch vom Inhalt abzuheben.
Das Bottom-Sheet selbst nutzt x-transition:enter mit einer Transform-Animation von translate-y-full zu translate-y-0, kombiniert mit einem halbtransparenten Backdrop, der bei Klick das Sheet schließt. Der Fokus wird beim Öffnen programmatisch auf die erste Sortieroption gesetzt, und @keydown.escape.window schließt das Sheet unabhängig davon, wo sich der Fokus gerade befindet, was für die Bedienbarkeit auf Touch-Geräten mit angeschlossener Tastatur relevant ist.
8. GraphQL/Headless-Aspekte der Kategorieseiten-Toolbar
Für Custom-Frontends oder PWA-Erweiterungen, die nicht auf dem serverseitig gerenderten Hyvä-Template aufsetzen, bildet die products-GraphQL-Query dieselbe Toolbar-Logik ab, die die Hyvä-Kategorieseite serverseitig nutzt. Der sort-Input-Typ erlaubt genau die Felder, die serverseitig used_for_sort_by gesetzt haben, sodass Frontend und Backend dieselbe Quelle der Wahrheit für verfügbare Sortierfelder teilen.
Die Parameter pageSize und currentPage übernehmen die Rolle von Limiter und Pager. Ein Headless-Frontend sollte diese Werte in der URL des Custom-Frontends spiegeln, etwa als Query-Parameter, damit Deep-Links und Browser-Navigation (vor/zurück) konsistent funktionieren, genau wie bei der serverseitig gerenderten Kategorieseiten-Toolbar. Der View-Mode-Umschalter hat in GraphQL kein Äquivalent, da er reines Frontend-Rendering ohne Serverbezug ist.
# GraphQL query mirroring the Hyva category page toolbar: sort, pagination, page size
query CategoryProductsToolbar(
$categoryId: String!
$currentPage: Int = 1
$pageSize: Int = 24
$sort: ProductAttributeSortInput
) {
products(
filter: { category_id: { eq: $categoryId } }
currentPage: $currentPage
pageSize: $pageSize
sort: $sort
) {
total_count
page_info {
current_page
page_size
total_pages
}
items {
sku
name
price_range {
minimum_price {
final_price {
value
currency
}
}
}
}
sort_fields {
default
options {
value
label
}
}
}
}
9. Performance und Full-Page-Cache-Kompatibilität
Die Full-Page-Cache-Kompatibilität der Hyvä-Kategorieseite hängt entscheidend davon ab, welche Toolbar-Zustände serverseitig und welche clientseitig verwaltet werden. Sortierung und Pagination erzeugen unterschiedliche Query-Strings (product_list_order, product_list_dir, p, product_list_limit), und jede dieser URLs wird vom Full Page Cache als eigener Cache-Eintrag behandelt. Das ist gewollt: Der Server liefert für jede Kombination korrekt sortierte, korrekt paginierte HTML-Antworten, die vollständig cachebar bleiben.
Der View-Mode-Umschalter dagegen verändert bewusst keine URL und keinen Request. Würde die Grid/List-Umschaltung serverseitig über eine Session-Variable gesteuert, müsste entweder jede Kategorieseite in zwei Cache-Varianten gehalten werden, oder die Session-Abhängigkeit würde die Seite komplett aus dem Full Page Cache herausnehmen. Die clientseitige Lösung über Alpine und localStorage umgeht dieses Problem vollständig, weil sie nach der Auslieferung des gecachten HTML im Browser greift, ohne den Cache-Schlüssel zu beeinflussen.
Für die Kategorieseiten-Toolbar insgesamt gilt deshalb die Faustregel: Alles, was die Reihenfolge oder Menge der ausgelieferten Produkte verändert, gehört in die URL und bleibt serverseitig cachebar. Alles, was nur die Darstellung identischer Daten betrifft, gehört auf die Client-Seite und bleibt für den Full Page Cache unsichtbar.
<?xml version="1.0"?>
<!--
Layout override for the Hyva category page toolbar.
Located at: app/design/frontend/Mironsoft/default/Magento_Catalog/layout/catalog_category_view.xml
-->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceBlock name="category.products">
<arguments>
<argument name="viewModels" xsi:type="array">
<item name="sort_options" xsi:type="object">
Mironsoft\CategoryToolbar\ViewModel\SortOptionsViewModel
</item>
</argument>
</arguments>
</referenceBlock>
<referenceBlock name="product_list_toolbar">
<arguments>
<argument name="template" xsi:type="string">
Magento_Catalog::product/list/toolbar.phtml
</argument>
</arguments>
</referenceBlock>
</body>
</page>
Mironsoft
Hyvä-Theme-Entwicklung, Toolbar-Anpassungen und Magento-Performance
Kategorieseiten-Toolbar, die zu eurem Shop passt?
Wir passen Sortierung, View-Mode-Umschalter und Pagination der Hyvä-Kategorieseite an eure Anforderungen an, mit ViewModels, Alpine.js-Komponenten und vollständiger Full-Page-Cache-Kompatibilität.
Toolbar-Audit
Analyse der bestehenden Kategorieseiten-Toolbar auf FPC-Kompatibilität und Barrierefreiheit
Alpine-Komponenten
Sortier-Dropdown, View-Mode-Umschalter und Mobile-Bottom-Sheet nach Maß
GraphQL-Integration
Sort- und Pagination-Queries für Headless-Frontends und PWA-Erweiterungen
10. Zusammenfassung
Die Hyvä-Kategorieseite ersetzt Knockout-Bindings und UI-Components-Grids durch einen einzigen serverseitig gerenderten Toolbar-Block mit Alpine.js für die interaktiven Anteile. Sortierfelder werden über used_for_sort_by und ein dediziertes ViewModel gesteuert, statt Logik im Template zu verstreuen. Der Grid/List-Umschalter bleibt bewusst clientseitig, weil er nur die Darstellung identischer Produktdaten betrifft und keinen neuen Request braucht.
Pagination und Items-per-page-Selector erzeugen dagegen unterschiedliche URLs und bleiben damit serverseitig cachebar. Für Headless-Szenarien bildet die products-GraphQL-Query dieselbe Sortier- und Pagination-Logik ab wie die serverseitig gerenderte Kategorieseiten-Toolbar. Wer diese Trennung zwischen serverseitigem Zustand und clientseitiger Darstellung konsequent einhält, bekommt eine Toolbar, die sich frei gestalten lässt, ohne den Full Page Cache zu gefährden.
Hyvä-Kategorieseite: Toolbar auf einen Blick
Sortierung
Toolbar-Block plus ViewModel steuern Sortierfelder über used_for_sort_by. Alpine ersetzt den nativen Select durch ein tastaturbedienbares Dropdown.
Grid/List-Umschalter
Rein clientseitig über Alpine und localStorage, ergänzt um ein Cookie für den initialen Server-Render ohne Layout-Sprung.
Pagination
Items-per-page und Pager erzeugen eigene URLs, oben und unten in der Produktliste, vollständig serverseitig gerendert.
FPC-Kompatibilität
Serverzustand geht in die URL, Darstellungszustand bleibt im Browser. So bleibt jede Sortier- und Pagination-Variante cachebar.