Skip to content

Katalog-Route-Reads auf cachebare Store-API-GETs umstellen (Cache-Rework) #8

Description

@dm-heinze

Problem Statement

Als Shopbetreiber/Entwickler will ich, dass die Katalog-Seiten (SEO-URL-Auflösung, Kategorie-, Produkt- und Landing-Entitäten, Navigation) vom neu eingeführten Shopware-http_cache bedient werden. Bisher war die Store-API nicht cachebar, weshalb nuxtware einen eigenen Nitro-Cache-Layer (cachedFunction) für SEO-URL und Navigation gebaut hat, während Kategorie- und Produkt-Reads ungecacht per POST an die Store-API gehen.

Mit dem Shopware Cache-Rework (Feature-Flag CACHE_REWORK, Default ab 6.8) sind nicht-mutierende Store-API-Routen per GET mit komprimierter Criteria (?_criteria) cachebar. Im Betrieb wird Varnish entfernt und durch Shopwares eingebauten http_cache ersetzt. Solange nuxtware seine Katalog-Reads aber per POST absetzt und parallel einen eigenen, jetzt redundanten Cache betreibt, greift der neue http_cache überhaupt nicht: kein Performance-Gewinn, doppelter Cache-Code, unnötige Backend-Last.

Solution

Die Katalog-Route-Reads, die nicht an die Shopware-Customer-Session gebunden sind, werden auf ihre cachebaren GET-Varianten umgestellt, damit der Shopware-http_cache sie ausliefert; der redundante Nitro-Cache-Layer für SEO-URL und Navigation wird ersatzlos entfernt.

  • Wo die shopware-frontends-Composables die GET-Variante bereits abbilden (SEO-URL, Navigation, Produkt-Detail), genügt das Opt-in-Flag cacheableReads.
  • Wo die SDK die GET-Variante nur wegen einer Typ-Lücke (_criteria auf GET nicht getippt) noch auf POST belässt, obwohl die Store-API-Route GET beherrscht (Kategorie-Detail, Landing-Detail — verifiziert an CategoryRoute/LandingPageRoute: methods:[GET,POST], _httpCache=>true), wird per offiziellem Composable-Override (Wrap-and-Spread) auf GET umgestellt.
  • Die HTTP-Methode wird pro Request nach der Criteria gewählt: GET (cachebar) nur für „cachebare Listing-Queries" (Query-Parameter ausschließlich aus einer Allow-List, initial nur Pagination p), sonst POST. SSR sendet immer die volle Criteria und rendert immer das vollständige, angeforderte Ergebnis — die Methode beeinflusst nur die Cachebarkeit, nie den gerenderten DOM. Damit bleiben Hydration-Korrektheit und Crawler-/No-JS-Sicht erhalten, während unbounded Filter-Permutationen die Cache-Hit-Rate nicht verwässern.

Siehe ADR docs/adr/0001-store-api-catalog-caching.md und Glossar CONTEXT.md.

User Stories

  1. Als Shopbetreiber möchte ich, dass anonyme Kategorie-Seitenaufrufe vom Shopware-http_cache bedient werden, damit die Store-API-Last und die Antwortzeiten sinken.
  2. Als Shopbetreiber möchte ich, dass anonyme Produkt-Detailseiten vom http_cache bedient werden, damit häufig aufgerufene Produkte nicht bei jedem Request neu berechnet werden.
  3. Als Shopbetreiber möchte ich, dass die SEO-URL-Auflösung cachebar ist, damit die teuerste Route (jeder Katalog-Aufruf beginnt damit) nicht ungecacht durchschlägt.
  4. Als Shopbetreiber möchte ich, dass die Navigations-/Menü-Daten cachebar sind, damit sie nicht pro Request neu geladen werden.
  5. Als Shopbetreiber möchte ich, dass Landing-Pages cachebar sind, damit Kampagnen-Seiten unter Last stabil bleiben.
  6. Als Entwickler möchte ich den redundanten Nitro-Cache-Layer (SEO-URL + Navigation) vollständig entfernen, damit es nur noch eine Cache-Autorität pro Ebene gibt und weniger Code zu pflegen ist.
  7. Als Entwickler möchte ich cacheableReads in nuxt.config aktivieren, damit die abgedeckten Composables (SEO-URL, Navigation, Produkt-Detail) automatisch auf GET umschalten.
  8. Als Entwickler möchte ich useCategorySearch().search per offiziellem Wrap-and-Spread-Override auf die GET-Variante der Kategorie-Detail-Route umstellen, ohne die restliche Composable-API zu übernehmen.
  9. Als Entwickler möchte ich useLandingSearch().search analog auf GET überschreiben, weil die Store-API-Route GET beherrscht, auch wenn die Release Notes sie nicht listen.
  10. Als Entwickler möchte ich, dass der Override die HTTP-Methode anhand der Query-Parameter wählt, damit nur cachebare Anfragen als GET laufen.
  11. Als Entwickler möchte ich eine zentrale, leicht erweiterbare Allow-List (CACHEABLE_LISTING_PARAMS, initial {'p'}), damit später weitere cachebare Parameter ohne Logikänderung ergänzt werden können.
  12. Als Shop-Kunde möchte ich beim Öffnen einer gefilterten/sortierten Kategorie-URL sofort das korrekte, vollständig serverseitig gerenderte Ergebnis sehen, damit es keine Hydration-Sprünge gibt.
  13. Als Suchmaschinen-Crawler (ohne Browser) möchte ich im SSR-DOM exakt das in der URL angeforderte Katalog-Ergebnis sehen, damit gefilterte/paginierte Views korrekt erfasst werden.
  14. Als Shop-Kunde möchte ich, dass eine paginierte Kategorie-Seite (?p=2) cachebar und schnell ist, weil paginierte Views ein bounded, häufig genutztes Muster sind.
  15. Als Shop-Kunde möchte ich Filter/Sortierung/Limiter interaktiv anwenden können, wobei diese (client-seitigen) Anfragen per POST laufen und nicht gecacht werden müssen.
  16. Als Entwickler möchte ich, dass das Basis- und paginierte Produkt-Listing über den cmsPage-Slot der Kategorie-Antwort mit-gecacht wird, ohne einen separaten Listing-Call einzuführen.
  17. Als Entwickler möchte ich, dass [...dynamic].vue die SEO-Auflösung über useNavigationSearch().resolvePath() macht und sowohl ein rohes SeoUrl-Element als auch ein bereits aufgelöstes {routeName, foreignKey} verarbeitet.
  18. Als Entwickler möchte ich, dass die technische-URL-Sonderbehandlung (/navigation/…, /detail/…, /landingPage/…) erhalten bleibt (durch resolvePath als Superset abgedeckt), damit direkte technische URLs weiter funktionieren.
  19. Als Entwickler möchte ich, dass die Sprach-/Währungs-/Kontext-Header (sw-language-id, sw-currency-id, sw-context-hash) im SSR stabil gesendet werden, damit der Cache-Key nicht fragmentiert.
  20. Als Consumer-Projekt, das die nuxtware-Layer erweitert, möchte ich das Kategorie-/Landing-Override weiter per eigenem Composable-Shadowing anpassen können, weil der offizielle Override-Mechanismus die Layer-Präzedenz respektiert.
  21. Als Entwickler möchte ich die Deps (composables 1.12, api-client 1.5, nuxt-module 1.5) upgraden und @shopware/api-gen gegen ein CACHE_REWORK-Backend neu generieren, damit die ...Get-Operationen getippt sind.
  22. Als Entwickler möchte ich die _criteria-Typ-Lücke auf den GET-Detail-Routen mit einem schmalen, dokumentierten Cast überbrücken, damit der Typecheck nicht bricht.
  23. Als Betreiber ohne shared Cache im SSR-Pfad möchte ich verstehen, dass die GET-Umstellung ohne http_cache wirkungslos ist, damit die Voraussetzung explizit ist.
  24. Als Entwickler möchte ich die Umstellung so kapseln, dass die Overrides trivial entfernbar sind, sobald shopware-frontends die getippten GET-Detail-Varianten offiziell nachzieht.

Implementation Decisions

  • Zielarchitektur: Browser → Nitro [nuxt-multi-cache] → Shopware [http_cache] → Store-API. Der eigene Nitro-cachedFunction-Layer entfällt; nuxt-multi-cache (SSR-Page-Cache) bleibt unverändert.

  • Dependency-Upgrade: @shopware/composables@1.12, @shopware/api-client@1.5, @shopware/nuxt-module@1.5 (+ passende helpers). cacheableReads: true unter shopware in nuxt.config. Store-API-Typen via @shopware/api-gen neu generieren.

  • Entfernen (redundanter Nitro-Cache): die Nitro-Routen für /seo-url und /navigation, die zugehörigen gecachten Server-Utils, das eigene useNavigationCached-Composable und die apiCacheLifetime-Konfiguration.

  • SEO-URL: Auflösung über useNavigationSearch().resolvePath(); die aufrufende Katalog-Route verarbeitet beide Rückgabeformen (SeoUrl-Element oder {routeName, foreignKey}) und behält das bestehende 404-Verhalten.

  • Navigation: Umstieg von useNavigationCached auf das Framework-useNavigation().

  • Produkt-Detail: useProductSearch().search schaltet über cacheableReads automatisch auf GET; bestehende Associations (withCmsAssociations, manufacturer/media/cover) wandern in _criteria.

  • Kategorie-Detail & Landing-Detail: offizieller Composable-Override (Datei-Shadowing, Wrap-and-Spread) — nur search ersetzt, Rest via ...core. Der Override repliziert das Framework-Muster cacheableReads ? invoke(get, {query:{_criteria}}) : invoke(post, {body}). _criteria wird wegen der SDK-Typ-Lücke schmal gecastet; die intern nicht-exportierte cmsAssociations-Konstante wird nachgebaut.

  • Methoden-Wahl nach Criteria (zentrale Regel): Ein Prädikat isCacheableListingQuery(query) liefert true, wenn alle Query-Keys in CACHEABLE_LISTING_PARAMS (initial new Set(['p'])) enthalten sind. Nur dann GET, sonst POST. Gilt für den Kategorie-Override; Landing-Requests haben typischerweise keine Listing-Params → GET. Ableitung aus der Grill-Session:

    keine Query            → GET   (Basis, cachebar)
    ?p=2                    → GET   (bounded: Seite 1..N)
    ?order=…               → POST  (korrekt gerendert, nicht gecacht)
    ?properties=…&min-price → POST
    
  • Interaktives Listing: Filter/Sorter/Limiter/Pagination-Klicks laufen weiter client-seitig über useCategoryListing().search (POST). Basis- und paginiertes Listing werden über den cmsPage-Slot der Kategorie-Antwort mit-gecacht — kein separater Listing-Call.

  • API-Contract: Alle Katalog-Reads enden in apiClient.invoke(operationString, options); operationString kodiert Operation + Methode + Pfad (z.B. readCategory get /category/{navigationId}), Criteria als query._criteria (GET) bzw. body (POST).

Testing Decisions

  • Was ist ein guter Test: nur externes Verhalten prüfen — bei gegebener Eingabe welche Store-API-Operation und -Methode aufgerufen wird — nicht die interne Implementierung. Kein Testen privater Helfer als Selbstzweck.
  • Primäre Seam (Vitest + Mocks): useShopwareContext().apiClient.invoke mocken und asserten, dass die Overrides pro Eingabe die korrekte Operation wählen:
    • Kategorie ohne Query → readCategory get /category/{navigationId} mit query._criteria.
    • Kategorie mit ?p=2 → GET.
    • Kategorie mit ?order=… / Filter → readCategory post /category/{navigationId} mit body.
    • Landing → GET.
    • (Mit cacheableReads: false → durchweg POST, Backwards-Compat.)
      Diese eine Seam deckt SEO-URL, Navigation, Produkt, Kategorie und Landing ab.
  • Integrationstests: als separate TypeScript-Skripte, außerhalb von Vitest und nicht in CI/CD, gegen ein echtes CACHE_REWORK-Backend. Prüfen: GET-Kategorie/Landing liefern die render-fertige cmsPage, _criteria wird zur Laufzeit angewendet, und die Antworten tragen die erwarteten Cache-Control-Header.
  • Neue Test-Infrastruktur: Vitest (+ @nuxt/test-utils soweit nötig) wird eingeführt — das Repo hat aktuell keine automatisierten Tests; es gibt daher keine Prior Art. Bestehende Quality-Gates (npx nuxt typecheck, npm lint) bleiben ergänzend.

Out of Scope

  • Infra-Umstellung selbst (Varnish entfernen, Shopware-http_cache aktivieren/konfigurieren) — Betriebs-/Deployment-Aufgabe, Voraussetzung dieser Umstellung.
  • Caching der interaktiven Produkt-Listing-Filter (Sortierung, Property-/Preis-Filter, geänderter Limiter) — bleiben bewusst POST/ungecacht (unbounded Permutationen). Später via Erweiterung von CACHEABLE_LISTING_PARAMS möglich.
  • Invalidations-Kohärenz zwischen nuxt-multi-cache (SSR-Seiten) und Shopware-http_cache (Store-API) — angrenzendes Thema, eigener Klärungsbedarf.
  • Customer-Session-gebundene Routen (Cart, Account, Checkout) — per Definition nicht cachebar, nicht Teil dieser Umstellung.

Further Notes

  • Voraussetzung: ein shared Cache (Shopware-http_cache) im SSR-Pfad; ohne ihn ist die GET-Umstellung wirkungslos.
  • Typ-Lücke: Kategorie-/Landing-Override hängen an fehlenden _criteria-Typen auf den GET-Detail-Routen — zu entfernen, sobald shopware-frontends die getippten GET-Varianten liefert. Kandidat für ein Upstream-Issue/-PR (deckt sich mit dem Roadmap-Punkt „Create GH Issue for question about cache rework").
  • Roadmap: entspricht docs/roadmap.md → Performance → „Cache rework implementation / Catalog Requests will be converted from POST to GET".
  • Backwards-Compat: cacheableReads ist per Default aus; Projekte ohne http_cache können auf POST bleiben (die Overrides fallen dann automatisch auf POST zurück).

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions