Snippet-Vorlagen entwickeln

Nutzen Sie Snippet-Vorlagen, um freigegebene Produktdaten als vorgerenderte HTML-Artefakte für Produktkarten, Teaser oder Detailbereiche bereitzustellen. Snippets kapseln Darstellung. Sie sind keine Quelle für Preise, Verfügbarkeit, Checkout-Regeln oder Warenkorbentscheidungen.

Die fachliche Pflege von Produktinhalten ist in Produktinhalte pflegen beschrieben. Die Konfiguration von Inhaltsschemata und Profilen steht in PIM-Inhalte konfigurieren.

Diese Seite ist die technische Hauptseite für Frontend-Entwickler. Sie beschreibt, welche Daten vor dem Template-Bau feststehen müssen, welche Felder im Snippet-ViewModel verfügbar sind, wie Sie fehlende Inhalte defensiv behandeln und wie die Storefront ein gebautes Artefakt einbindet.

Voraussetzungen

Klären Sie vor der Template-Entwicklung:

  • Der Zielmandant wurde mit Tenant Init vorbereitet. Für Snippet-Arbeit muss die PIM-Basis initialisiert sein; für Hintergrundjobs und Rebuilds müssen die zugehörigen tenant-lokalen Jobs vorhanden sein. System > Betrieb > Betriebsbereitschaft zeigt keine blockierenden systemweiten Hinweise.
  • Der aktuelle Storefront-Kontext liefert Sales Channel, Locale und Währung für die spätere Einbindung.
  • Die Produktvarianten haben freigegebene Produktinhalte für den benötigten Zweck, die Sprache und bei Bedarf den Vertriebskanal.
  • Inhaltsschema und Inhaltsprofil passen zur Produktfamilie und zum Veröffentlichungsziel.
  • Nach relevanten Inhalts- oder Template-Änderungen wird ein Snippet-Rebuild gestartet.

Wenn eine dieser Voraussetzungen fehlt, bleibt die öffentliche Storefront trotz Template technisch korrekt. Rendern Sie dann aus Public Catalog, Detail und Pricing oder zeigen Sie einen definierten Ladezustand.

Für serverseitig ausgelieferte Produktdetailseiten entscheidet das Site-Template, welche Snippet-Bereiche Pflicht sind. Fehlt ein Required-Snippet für Tenant, Variante, Locale und SalesChannel, liefert die Detailseite 503, bis der Snippet-Rebuild das Artefakt erzeugt hat. Optionale Bereiche können leer bleiben. Der 503-Fall wird als kritischer Storefront-Telemetry-Fehler gemeldet. Nutzen Sie die Telemetry-Instanz, um Route, Variante, Locale, Sales Channel und fehlende Snippet-Namen zu prüfen.

Planen Sie die Besucheransicht dieses Zustands als eigene Site-Fehlerseite. Die dynamische Produktdetail-Runtime sucht im veröffentlichten Release nach global/<default-locale>/503.html. Fehlt diese Datei, erscheint die Workspace-Standardfehlerseite. Bauen Sie die 503-Seite statisch im Shop- Design und vermeiden Sie dort ProductSnippets, damit die Fehlerseite nicht von dem Artefakt abhängt, dessen Fehlen den Fehler auslöst. Setzen Sie die Seite auf noindex und schließen Sie sie aus der Sitemap aus.

Arbeitsablauf

Bauen Sie ein Produkt-Snippet in dieser Reihenfolge:

  1. Wählen Sie den Ausgabeort. Eine Produktkarte, ein Hero-Bereich, technische Daten und Empfehlungen sind unterschiedliche Snippets mit unterschiedlichen Namen.
  2. Ermitteln Sie den Storefront-Kontext: Tenant, Produktvariante, Locale, Sales Channel und Währung. Der Client darf diese Werte nicht raten.
  3. Prüfen Sie, ob die Variante für diesen Kontext freigegebene Produktinhalte und bei Bedarf veröffentlichte Medien hat. Prüfen Sie eine passende ProductRoute nur dann, wenn die Variante eine eigene Browser-URL erhalten soll; eine fehlende Route verhindert weder Publication noch Snippet-Rebuild.
  4. Legen Sie den Snippet-Namen nach dem Ausgabeort fest, zum Beispiel product-detail-hero. Packen Sie Locale oder Sales Channel nicht in den Namen. Der Name ist groß-/kleinschreibungssensitiv und muss exakt mit productSnippet.name in der Site übereinstimmen.
  5. Entscheiden Sie, ob das Snippet optional oder required ist. Required passt nur, wenn die Detailseite ohne diesen Bereich fachlich nicht ausgeliefert werden soll.
  6. Bauen Sie das HTML-Template gegen das Preview-ViewModel. Lesen Sie optionale Felder mit index und with, damit fehlende Inhaltszwecke keinen Template-Fehler auslösen.
  7. Nutzen Sie den Preview-Tab mit einer realen resourceId, Locale und salesChannelId. Prüfen Sie gerendertes HTML und das zurückgelieferte viewModel.
  8. Starten Sie nach Inhalt-, Medien-, Scope- oder Template-Änderungen den Snippet-Rebuild für den betroffenen Scope.
  9. Binden Sie das gebaute Artefakt über den passenden Storefront-Pfad ein: serverseitig über productSnippet oder headless über die Snippet-Referenzen aus Public Catalog.

Die Aufgabe ist abgeschlossen, wenn Preview und Rebuild für denselben Scope erfolgreich sind und die öffentliche Produktseite oder Detailantwort das passende Snippet-Artefakt für Tenant, Variante, Locale und Sales Channel liefert.

Einsatzbereiche

Typische Vorlagen sind:

  • product-card für Listen und Suchergebnisse
  • product-teaser für Empfehlungen oder Startseitenbereiche
  • product-detail-summary für den oberen Bereich einer Produktdetailseite
  • product-options für Relationsgruppen wie Zubehör oder Erweiterungen
  • eigene Detailabschnitte für technische Daten, Lieferumfang, Kompatibilität oder Downloads

Planen Sie Vorlagen nach Ausgabeort, nicht nach einzelnen Produktfamilien. Eine Produktkarte sollte dieselbe Vorlage behalten, solange Layout und Ziel gleich bleiben.

Eine Storefront-Seite kann mehrere Produktbereiche enthalten. Trennen Sie diese Bereiche fachlich: Ein Hero-Snippet, ein Zubehörbereich, eine Cross-Sell-Leiste und ein Ersatzteilbereich sind getrennte Ausgabeorte, auch wenn sie auf derselben Seite stehen. Jeder Bereich braucht einen eigenen Snippet-Namen oder eine eigene öffentliche Datenabfrage, lädt seine Daten im aktuellen Storefront-Kontext und behandelt leere Ergebnisse getrennt. So bleibt sichtbar, ob nur Zubehör fehlt, ob ein optionaler Empfehlungsbereich leer ist oder ob der produkttragende Pflichtbereich nicht bereit ist.

Wenn Sie mehrere dynamische Produktbereiche im Client hydratisieren, behandeln Sie jeden Bereich als eigene Instanz. Initialisieren Sie nicht nur den ersten passenden Katalog- oder Produktbereich einer Seite und teilen Sie keinen impliziten globalen Zustand zwischen den Bereichen. Jeder Bereich sollte aus seinem Markup oder seiner Konfiguration erkennen lassen, welche Produkte, Relationen oder Snippet-Artefakte er laden soll.

AusgabeortTypischer NameRequired?
Such- oder Kategorielisteproduct-cardNein
Produktdetail-Heroproduct-detail-heroNur wenn die serverseitige Detailseite ohne Hero nicht sinnvoll ist
Redaktioneller Detailinhaltproduct-detail-contentMeist optional
Technische Datenproduct-detail-specsOptional oder required je nach Produkttyp
Empfehlungen oder Zubehörproduct-detail-recommendationsMeist optional

Vorlage anlegen

Öffnen Sie PIM > Konfiguration > Snippet-Vorlagen, um eine Vorlage zu pflegen.

Legen Sie fest:

  • Ressourcentyp variant
  • technischer Name der Vorlage
  • optional Sprache
  • optional Vertriebskanal
  • HTML-Template
  • optionales Transform-Skript
  • Aktivstatus

Snippet-Namen tragen keine Scope-Bedeutung. Nutzen Sie nicht Namen wie product-card-de oder product-card-b2b-de, wenn Sprache oder Vertriebskanal über die Vorlage gesetzt werden sollen. Der gleiche Name darf als globales, locale-spezifisches, channel-spezifisches oder kombiniert gescoptes Template existieren. SnippetTemplate.Name ist dennoch ein exakter öffentlicher Artefakt-Pfadvertrag: Product-Detail-Hero und product-detail-hero sind unterschiedliche Namen. Beim Rebuild gewinnt der spezifischste Treffer:

TrefferPriorität
Sales Channel und Locale passen1
Sales Channel passt, Locale leer2
Locale passt, Sales Channel leer3
Sales Channel und Locale leer4

Sprache, Vertriebskanal und globaler Scope

Der Scope einer Snippet-Vorlage besteht aus Sprache und Vertriebskanal. Beide Felder sind optional. Global bedeutet hier: Die Vorlage hat keine Sprache und keinen Vertriebskanal. Sie ist der Fallback, wenn für den aktuellen Storefront-Kontext keine spezifischere aktive Vorlage existiert.

VorlageSpracheVertriebskanalWann verwenden?
GloballeerleerDas Markup ist für alle Sprachen und Kanäle gleich und liest sichtbare Texte aus Produktinhalten oder Übersetzungen.
SprachspezifischgesetztleerLayout, feste Labels oder Textreihenfolge unterscheiden sich pro Sprache, aber nicht pro Kanal.
Channel-spezifischleergesetztEin Vertriebskanal braucht eine andere Darstellung, aber die Sprache ändert das Template nicht.
KombiniertgesetztgesetztEin bestimmter Kanal und eine bestimmte Sprache brauchen eine eigene Darstellung.

Ein globales Template übersetzt nichts automatisch. Wenn das Template feste sichtbare Wörter enthält, zum Beispiel Mehr erfahren, Technische Daten oder Lieferumfang, müssen diese Texte aus einem übersetzten Inhalt, Übersetzungsbündel oder aus sprachspezifischen Templates kommen. Nutzen Sie global nur für Markup, das mit jeder späteren Storefront-Sprache korrekt bleibt.

Ein globales Template erzeugt außerdem nicht automatisch Artefakte für alle Sprachen und Vertriebskanäle. Der Rebuild materialisiert Artefakte für die fachlich vorhandenen Scopes der Varianten. Wenn ein Produkt in de-DE und en-US sichtbar sein soll, müssen Produktinhalte, Routen, Veröffentlichungen und Rebuild-Scope zu diesen Sprachen passen. Das globale Template kann dann als Darstellungsvorlage dienen; es ersetzt nicht die fehlenden Sprachdaten.

Unterscheiden Sie den globalen Template-Fallback vom Rebuild ohne Filter. Ein tenantweiter Rebuild ohne --locale und ohne --sales-channel-id bedeutet: Workspace baut die verfügbaren Snippet-Scopes des Tenants neu. Das ist nicht dasselbe wie ein globales Template und nicht die Anweisung, nur ungescopte Artefakte zu erzeugen.

Template-Struktur

Ein wartbares Snippet erfüllt diese Regeln:

  • Es enthält semantisches HTML für den Zielbereich.
  • Es zeigt optionale Abschnitte nur an, wenn Daten vorhanden sind.
  • Es rendert redaktionelle HTML-Felder nur an dafür vorgesehenen Stellen.
  • Es rendert Produktrelationen nur aus public-safe Recommendation-Gruppen.
  • Es nutzt keine internen Admin-Daten.
  • Es berechnet keine Preise, Verfügbarkeit oder Checkout-Entscheidungen.

Beispiel für eine Produktkarte:

html
<article class="product-card">
  <h2>{{ .name }}</h2>
  {{ if .description_short }}
    <p>{{ .description_short }}</p>
  {{ end }}
</article>

Beispiel für einen Detailabschnitt:

html
<section class="product-detail-section">
  <h2>Beschreibung</h2>
  <div class="product-detail-section__content">
    {{ .description_short }}
  </div>
</section>

Snippet-Templates laufen über Go html/template. Normale Platzhalter werden kontextabhängig escaped. Rechnen Sie deshalb nicht damit, dass ein gepflegter String ungeprüft als HTML-Markup ausgegeben wird. Wenn ein Detailbereich Markup braucht, muss der Inhalt vor der Auslieferung fachlich und technisch als dafür geeigneter Vertrag bereitstehen.

ViewModel verstehen

Die Preview und der Snippet-Rebuild verwenden denselben ViewModel-Vertrag. Ohne Lua-Transform sieht das Template die Felder der Variantenprojektion. Die wichtigsten Felder sind:

FeldBedeutungHinweis
idProduktvarianten-IDFür Datenattribute oder Debugging, nicht als sichtbarer Produktname
tenant_idTenant-IDNur verwenden, wenn ein öffentlicher Asset-Pfad sie braucht
family_idProduktfamilien-IDNicht als Kategorie- oder URL-Ersatz nutzen
skuSKU der VarianteSichtbar nur, wenn der Shop SKU anzeigen soll
title, nameVariantenname oder public-safe NameGeeignet für einfache Karten und Überschriften
base_unit_codeBasiseinheitNur anzeigen, wenn fachlich relevant
description_shortKurzbeschreibung aus der Rolle primary_product_contentKann leer sein
product_urlLive-Route der Variante im aktuellen Storefront-KontextKann leer sein, wenn keine passende ProductRoute live ist
primary_image_urlÖffentlich auslieferbarer Alias auf den Medienzweck primaryKann leer sein
price_displayFür den Snippet-Scope fertig formatierter öffentlicher AnzeigenpreisLeer, wenn kein sichtbarer Preis vorliegt
attributesVariantenattribute als ObjektNur für Darstellung, nicht für Produktlogik
contentInhalte nach ZweckHauptpfad für strukturierte Produktinhalte
shippingVersand- und Logistik-Facts für VersandhinweiseNur für rechtliche Hinweise und Darstellung, nicht für Kostenberechnung
shipping_requiredKurzform von shipping.requiredFür einfache Template-Bedingungen
recommendation_groupsPublic-safe ProduktrelationenNur gefüllt, wenn Scope mit Sales Channel und Locale auflösbar ist
public_priceVorgelöster öffentlicher anonymer Preisstatus aus dem aktuellen Sales-Channel-ScopeNur bei Sales-Channel-Scope vorhanden; für Darstellung, nicht für Checkout-Entscheidungen
localeNormalisierte Locale des ScopesNur vorhanden, wenn ein Locale-Scope verwendet wird
sales_channel_idSales-Channel-ID des ScopesNur vorhanden, wenn ein Channel-Scope verwendet wird

Das ViewModel kann Kompatibilitätsfelder wie price_amount, price_currency, inventory_physical oder in_stock enthalten. Bei Sales-Channel-scoped Rebuilds stammen price_amount und price_currency aus dem öffentlichen anonymen Pricing-Scope des Vertriebskanals: SalesChannel -> CommerceProfile -> defaultPriceListId. Ohne Sales-Channel- Scope bleiben die Kompatibilitätsfelder ein Preview-/Legacy-Fallback. Nutzen Sie für sichtbare HTML-Preise bevorzugt price_display oder bei detaillierter Darstellung public_price.amount_display, public_price.unit_net_display und public_price.unit_gross_display. Diese Felder sind bereits für Locale und Währung des Snippet-Scopes formatiert. Die Rohfelder price_amount, public_price.amount, public_price.unit_net und public_price.unit_gross bleiben maschinenlesbare Dezimalwerte und sind nicht lokalisiert. Die serverseitige Display-Formatierung nutzt den Währungskatalog des Mandanten. Symbol und Nachkommastellen kommen aus der aktiven Tenant-Währung; Snippets leiten die Währung nicht aus der Sprache ab und führen keine automatische Währungsumrechnung aus. Wenn public_price.visible false ist, enthält public_price Status und Währung, aber keine Betrag-Felder wie amount, unit_net oder unit_gross. Verwenden Sie Snippet-Felder nur für Darstellung. Warenkorb, Checkout und kundenspezifische Preise müssen weiterhin über die Public-Storefront- und Pricing-Verträge aufgelöst werden.

Die Formatierung folgt dem Storefront-Scope, nicht nur der Sprache. Ein Shop kann zum Beispiel de-DE und de-AT als gleiche Sprache in verschiedenen Märkten nutzen. Dann entscheidet der jeweilige Sales Channel über Währung, Preisliste und Steuerkontext, während die Locale die Schreibweise des Betrags steuert. Ein Schweizer Shop kann dagegen de-CH, fr-CH und en-CH als mehrere Sprachen im selben Markt nutzen. Dann bleiben Markt und Währung gleich, aber sichtbare Zahlenschreibweise und feste Texte kommen aus der passenden Locale. Packen Sie diese Unterscheidung nicht in Snippet-Namen; setzen Sie den richtigen Rebuild-Scope aus --locale und --sales-channel-id.

Versandhinweise anzeigen

Nutzen Sie shipping.charge_mode, wenn ein Snippet einen Versandkostenhinweis anzeigen soll. shipping.required bleibt die einfache physische Versandpflicht. In v1 gilt:

text
shipping.required = true genau dann, wenn die Variante physisch ist.

Das Snippet berechnet keine konkreten Versandkosten, keine Lieferzonen und keine Warenkorbregeln. Wenn ein digitales Bundle, eine Dienstleistung oder eine Lizenz einen Hardwareanteil hat, muss die Variante in den Produktdaten als physisch beziehungsweise physisch relevant modelliert sein.

shipping.charge_mode beschreibt, welchen allgemeinen Hinweis das Snippet im aktuellen Sales-Channel-Scope anzeigen kann. Das Feld berechnet keinen konkreten Betrag und löst keine Lieferzone auf.

shipping.tags enthält nur die Versand-Tags aus der Logistik-Konfiguration der Variante. Normale Variant-Tags werden nicht als Versand-Facts übernommen und dürfen nicht als Ersatz für Versandregeln, Logistikprofile oder Tarife dienen.

WertBedeutung
plus_shippingFür den Shop-Scope ist ein positiver Versandkostenpfad gepflegt.
includedDie Variante ist physisch, aber im Shop-Scope ist kein positiver Versandkostenpfad gepflegt oder nur ein 0-EUR-Pfad vorhanden.
review_requiredEine passende Versandregel verlangt Prüfung statt direkter Online-Berechnung.
unknownDer Shop-Scope reicht nicht für einen belastbaren Hinweis, zum Beispiel ohne Sales Channel, Commerce-Profil, Default-Preisliste oder Default-Land.
not_requiredDie Variante ist nicht physisch versandpflichtig.

Beispiel:

html
{{ if eq .shipping.charge_mode "plus_shipping" }}
  <p class="product-detail__shipping-note">zzgl. Versandkosten</p>
{{ else if eq .shipping.charge_mode "included" }}
  <p class="product-detail__shipping-note">inkl. Versandkosten</p>
{{ end }}

Für einfache Vorlagen steht zusätzlich shipping_required als Kurzform von shipping.required bereit. Verwenden Sie diese Kurzform nicht als alleinige Quelle für den Wortlaut, wenn zwischen „zzgl. Versandkosten“ und „inkl. Versandkosten“ unterschieden werden soll.

Das Feld shipping.basis beschreibt die Produkt- oder Variantenbasis für den Hinweis. Es beschreibt nicht die Versandmethode und nicht, aus welcher Regel die Entscheidung stammt.

WertBedeutung
physicalPhysische Variante
digitalDigitale Variante
serviceDienstleistungsvariante
licenseLizenzvariante
bundleBundle-Variante
noneKeine spezifische Basis gesetzt

Die Versand-Facts sind für Templates stabil vorhanden:

FeldWert, wenn unbekannt oder nicht gesetzt
shipping.requiredfalse
shipping.basisnone
shipping.charge_modeunknown für physische Varianten ohne belastbaren Shop-Scope, sonst not_required
shipping.weight_gross_kgleerer String
shipping.package_count0
shipping.pallet_count0
shipping.logistics_profile_idleerer String
shipping.tagsleeres Array

shipping.tags gibt produkt- oder tenantseitige Labels aus den Logistikdaten durch. Diese Tags sind nicht global standardisiert. Verwenden Sie sie nur für Template-Logik, wenn der Tenant die Bedeutung stabil pflegt. Für allgemein gültige Hinweise verwenden Sie bevorzugt shipping.charge_mode, shipping.required und shipping.basis.

Felder defensiv lesen

Der Renderer nutzt missingkey=error. Ein direkter Punktpfad auf ein fehlendes Feld kann deshalb den Build oder die Preview abbrechen. Verwenden Sie für optionale Inhalte index und with.

html
{{ with .primary_image_url }}
  <img class="product-card__image" src="{{ . }}" alt="">
{{ end }}
html
{{ with $detail := index .content "website_detail" }}
  {{ with $headline := index $detail.fields "headline" }}
    <h2>{{ $headline }}</h2>
  {{ end }}
  {{ with $copy := index $detail.fields "shortDescription" }}
    <p>{{ $copy }}</p>
  {{ end }}
{{ end }}

Wenn ein Feld für den Ausgabeort fachlich verpflichtend ist, behandeln Sie das nicht im Template als stillen Fallback. Klären Sie Inhaltsschema, Inhaltsprofil, Produktinhalt und Veröffentlichung, damit der Rebuild das erwartete Feld im richtigen Scope sieht.

Inhaltszwecke verwenden

Root-Platzhalter wie title, name, description_short oder product_url kommen aus dem öffentlichen Rollenvertrag. Die Rolle primary_product_content wird über public.product_content.primary_purpose_key auf einen Purpose-Key gebunden, zum Beispiel product-overview. Wenn dieser ConfigValue leer ist, nutzt Workspace isPublicDefault nur noch als Legacy-Fallback. Nutzen Sie diese Root-Felder für bestehende Produktkarten und einfache Produktdetailbereiche.

Weitere Inhaltszwecke stehen im ViewModel unter content.<purposeKey>. Die gepflegten Schemafelder liegen unter fields:

html
{{ with $detail := index .content "website_detail" }}
  <h2>{{ index $detail.fields "name" }}</h2>
{{ end }}
{{ with $listing := index .content "marketplace_listing" }}
  <p>{{ index $listing.fields "headline" }}</p>
{{ end }}

Wenn ein Zweck- oder Feldschlüssel Zeichen enthält, die nicht als Punktpfad verwendbar sind, nutzen Sie ebenfalls die index-Schreibweise:

html
{{ index (index (index .content "marketplace-listing") "fields") "headline" }}

Jeder Zweck enthält zusätzlich Metadaten wie purpose_key, locale, sales_channel_id, content_schema_id, content_schema_version und product_url. Verwenden Sie Metadaten nur für Darstellung und Routing, nicht für Preis-, Bestands- oder Checkout-Entscheidungen.

Planen Sie Templates so, dass sie fachliche Inhaltsbereiche kombinieren können. Eine Produktdetailseite kann zum Beispiel den öffentlichen Hauptinhalt, technische Daten, Sicherheitshinweise und Downloads rendern. Das Template sollte optionale Zwecke nur ausgeben, wenn sie im ViewModel vorhanden sind. SEO-Metadaten gehören zur Rolle seo_metadata; Snippet-Templates sollen sie nur rendern, wenn der Ausgabeort das ausdrücklich braucht.

Erfinden Sie keine kanal- oder layoutgetriebenen Zwecknamen in Templates. Wenn ein Inhalt fachlich Sicherheitshinweis ist, lesen Sie content.safety_instructions; der Sales Channel entscheidet vorher, welcher kanalgebundene oder allgemeine Inhalt im ViewModel liegt. Wenn ein Marktplatzlisting andere Felder braucht, verwenden Sie einen fachlich passenden Marktplatzzweck oder ein kanalgebundenes Profil, nicht einen Template-Hilfsschlüssel.

Medienzwecke verwenden

Produktbilder, PDFs, Tabellen, CAD-Dateien und Videos werden im PIM nicht über Dateinamen fachlich erkannt. Der verbindliche Vertrag für Storefronts und Snippets ist der zentral gepflegte Medienzweck. Administratoren pflegen die verfügbaren Zwecke pro Tenant, Redakteure ordnen Produkt-Assets einem oder mehreren Zwecken zu.

Planen Sie vor der Template-Umsetzung, welche Zweck-Keys die Storefront erwartet. Typische Verträge sind:

Storefront-BereichZweck-Key-BeispielHinweis
Primärbildprimary und primary_image_urlmedia.primary[0] liefert das vollständige Media-Modell; primary_image_url ist der einfache Kompatibilitätsalias.
Hero-BereichheroFür große Produktbühnen und Headerbilder.
GaleriegalleryMehrere Assets sortiert ausgeben.
DetailansichtendetailNahaufnahmen, Varianten- oder Materialdetails.
LifestylelifestyleAnwendungsszenen und redaktionelle Produktbilder.
Downloadsdatasheet, manualPDF- und Dokument-Assets als Links rendern.
Technische Tabellentechnical_data, price_sheetCSV-, XLS- oder XLSX-Assets als Download rendern.
CAD-Datencad_download, drawingCAD- oder Zeichnungsdateien als Download rendern.
Videovideo, product_videoVideo-Assets oder videobezogene Medien ausgeben.

Templates dürfen Zweck-Keys nicht lokal erfinden oder aus Dateinamen ableiten. Wenn ein neuer Bereich benötigt wird, legen Produkt-, Content- und Frontend-Team zuerst den Zweck-Key fest, danach wird er im PIM-Katalog gepflegt und in der Vorlage verwendet. Für Pflichtbereiche kann die Produktbereitschaft über media.requiredPurposeKeys abgesichert werden.

Das Snippet-ViewModel enthält weiterhin primary_image_url. Der Wert zeigt auf eine public-safe Bild-Rendition aus dem Medienzweck primary; ältere Hauptbilddaten dienen nur als Kompatibilitätsfallback. Zusätzlich kann es Medien nach Zweck-Key gruppieren:

json
{
  "primary_image_url": "/assets/.../sku-123-front_1024x0.jpeg",
  "media": {
    "primary": [
      {
        "asset_id": "8f0b...",
        "purpose_key": "primary",
        "type": "image",
        "media_type": "image/jpeg",
        "url": "/assets/.../sku-123-front_1024x0.jpeg",
        "filename": "sku-123-front.jpg"
      }
    ],
    "hero": [
      {
        "asset_id": "8f0b...",
        "purpose_key": "hero",
        "type": "image",
        "media_type": "image/jpeg",
        "url": "/assets/.../hero_1024x0.jpeg",
        "filename": "sku-123-front.jpg",
        "title": "Frontansicht",
        "alternative_text": "Produkt in Frontansicht",
        "sort_order": 0,
        "renditions": [
          {
            "format": "jpeg",
            "media_type": "image/jpeg",
            "width": 1024,
            "url": "/assets/.../hero_1024x0.jpeg"
          },
          {
            "format": "webp",
            "media_type": "image/webp",
            "width": 1024,
            "url": "/assets/.../hero_1024x0.webp"
          }
        ],
        "picture": {
          "sources": [
            {
              "type": "image/webp",
              "src_set": "/assets/.../hero_640x0.webp 640w, /assets/.../hero_1024x0.webp 1024w",
              "sizes": "100vw"
            }
          ],
          "img": {
            "src": "/assets/.../hero_1024x0.jpeg",
            "src_set": "/assets/.../hero_640x0.jpeg 640w, /assets/.../hero_1024x0.jpeg 1024w",
            "alt": "Produkt in Frontansicht"
          }
        }
      }
    ]
  }
}

Für Bild-Assets zeigt url auf eine erzeugte JPEG-Rendition. renditions enthält die erzeugten Bildvarianten, picture.sources liefert WebP-Quellen für ein <picture>-Element und picture.img den JPEG-Fallback. Dokumente, Tabellen, CAD-Dateien und Videos verwenden denselben Asset-Vertrag ohne Bild-Renditions.

In Go-Templates lesen Sie die Mediengruppe defensiv mit index. Rendern Sie optionale Bereiche nur, wenn die Gruppe vorhanden ist:

html
{{ with $media := index . "media" }}
  {{ with $heroAssets := index $media "hero" }}
    {{ with $asset := index $heroAssets 0 }}
      {{ with $picture := $asset.picture }}
        <picture>
          {{ range $source := $picture.sources }}
            <source type="{{ $source.type }}" srcset="{{ $source.src_set }}" sizes="{{ $source.sizes }}">
          {{ end }}
          <img src="{{ $picture.img.src }}" srcset="{{ $picture.img.src_set }}" alt="{{ $picture.img.alt }}">
        </picture>
      {{ else }}
        <img src="{{ $asset.url }}" alt="{{ $asset.alternative_text }}">
      {{ end }}
    {{ end }}
  {{ end }}
{{ end }}

Für Galerien und Downloads arbeiten Sie mit derselben Gruppe und geben alle Assets in der sortierten Reihenfolge aus:

html
{{ with $media := index . "media" }}
  {{ with $gallery := index $media "gallery" }}
    <div class="product-gallery">
      {{ range $asset := $gallery }}
        <img src="{{ $asset.URL }}" alt="{{ $asset.AlternativeText }}">
      {{ end }}
    </div>
  {{ end }}

  {{ with $documents := index $media "datasheet" }}
    <ul class="product-downloads">
      {{ range $asset := $documents }}
        <li><a href="{{ $asset.URL }}">{{ or $asset.Title $asset.Filename }}</a></li>
      {{ end }}
    </ul>
  {{ end }}
{{ end }}

Wenn eine headless Storefront die Produktdaten als JSON verarbeitet, nutzt sie die JSON-Felder in snake_case, zum Beispiel url, media_type und alternative_text. Serverseitige Go-Templates greifen auf die exportierten Feldnamen wie URL, MediaType und AlternativeText zu.

Öffentliche URLs entstehen nur für freigegebene ProductAssets, deren StorageObject über die ProductAsset-Publish-Operation für öffentliche Delivery freigegeben wurde. Der Rebuild erzeugt oder erneuert Delivery-Artefakte, macht ein privates StorageObject aber nicht selbst öffentlich.

Ändern Sie Zweck-Keys nur kontrolliert. Ein umbenannter Zweck-Key ist aus Storefront-Sicht eine Vertragsänderung und benötigt eine abgestimmte Template-Anpassung sowie einen Rebuild der betroffenen Snippets. Nach Medien-, Zweck- oder Zuordnungsänderungen muss ebenfalls ein Rebuild laufen, wenn die Storefront vorgerenderte Snippet-Artefakte nutzt.

Lua-Transform verwenden

Nutzen Sie ein Transform-Skript nur, wenn die Vorlage ein kleines, stabiles ViewModel für die Darstellung braucht. Halten Sie den Transform deterministisch und frei von externen Nebenwirkungen.

Beispiel:

lua
local detail = resource.content and resource.content.website_detail or { fields = {} }
local fields = detail.fields or {}

result = {
  title = fields.name or resource.name,
  teaser = fields.shortDescription or resource.description_short
}

Wenn das Skript result setzt, rendert das HTML-Template nur dieses Ergebnis. Wenn Sie das ursprüngliche ViewModel erweitern möchten, ändern Sie view_model und lassen result leer. Kopieren Sie benötigte Felder bewusst, damit das Template nicht versehentlich von internen oder instabilen Daten abhängt.

Der Transform darf keine Preise, Verfügbarkeit, Warenkorbzustände oder interne Admin-Daten berechnen. Laden Sie solche Informationen in der Storefront über die öffentlichen Verträge.

Relationsgruppen anzeigen

Wenn eine Vorlage Produktrelationen rendert, nutzen Sie die im Snippet-ViewModel bereitgestellten recommendation_groups.

Die Gruppen verwenden technische Relationstypen wie accessory, cross_sell, up_sell, substitute, spare_part und bonus_item. Übersetzen Sie sichtbare Überschriften im Template oder in der Storefront, aber ändern Sie die technischen Keys nicht.

Beispiel:

html
{{ range .recommendation_groups }}
  <section class="product-recommendations">
    <h2>{{ .label }}</h2>
    <div class="product-recommendations__items">
      {{ range .items }}
        <article class="product-teaser">
          <h3>{{ .name }}</h3>
        </article>
      {{ end }}
    </div>
  </section>
{{ end }}

recommendation_groups sind leer, wenn der Rebuild keinen Sales-Channel- und Locale-Scope hat oder wenn die Zielvarianten im öffentlichen Katalog für diesen Scope nicht gelistet sind. Ein leeres Ergebnis ist kein Grund, interne PIM-Relationen direkt im Frontend nachzuladen.

Preview und Rebuild prüfen

Der Preview-Tab rendert das aktuelle Template gegen eine reale Variante. Geben Sie immer dieselbe Variante, Locale und salesChannelId an, die später auch die Storefront verwendet. Die Preview liefert HTML und viewModel zurück. Prüfen Sie auch globale Vorlagen mit einem konkreten Storefront-Scope. Nur so sehen Sie, ob die globale Vorlage für diese Sprache und diesen Vertriebskanal das richtige ViewModel erhält.

Prüfen Sie im viewModel:

  • Stimmt id mit der gewünschten Variante überein?
  • Sind locale und sales_channel_id für den geplanten Scope gesetzt?
  • Liegt der benötigte Inhalt unter content.<purposeKey>.fields?
  • Enthält product_url eine Live-Route, wenn das Snippet verlinkt?
  • Ist media.primary[0] oder primary_image_url gesetzt, wenn das Snippet ein einfaches Produktbild zeigen soll?
  • Enthält media.<purposeKey>[] die erwarteten Assets, wenn Hero, Galerie, Detailbilder, Downloads oder Videos über Medienzwecke gerendert werden?
  • Sind recommendation_groups vorhanden, wenn das Snippet Empfehlungen rendert?
  • Enthält variant_selection die erwarteten öffentlichen Achsen und Kombinationen, wenn das Snippet eine Variantenwahl rendert?

Nach Template-, Inhalts-, Medien- oder Scope-Änderungen muss ein Rebuild laufen. Ein Rebuild ohne passenden Scope erzeugt kein Artefakt für die konkrete Storefront. Wenn eine Detailseite danach weiter 503 liefert, prüfen Sie zuerst Tenant, Variante, Locale, Sales Channel, Snippet-Name und Required-Status.

Für sprach- oder channel-spezifische Korrekturen reicht oft ein selektiver Rebuild mit --locale oder --sales-channel-id. Nutzen Sie einen tenantweiten Rebuild, wenn eine globale Vorlage geändert wurde oder wenn mehrere Scopes betroffen sind.

Variantenwahl statisch rendern

Nutzen Sie variant_selection, wenn eine Produktdetailseite Größen, Farben oder andere Variantenachsen als vorgerendertes HTML ausliefern soll. Das Feld ist nur in einem konkreten Sales-Channel- und Locale-Scope verfügbar und enthält ausschließlich vollständige, public-safe Kombinationen derselben Produktfamilie.

text
variant_selection.active_variant_id
variant_selection.axes[].key
variant_selection.axes[].label
variant_selection.axes[].values[].value
variant_selection.axes[].values[].label
variant_selection.combinations[].variant_id
variant_selection.combinations[].values
variant_selection.combinations[].active
variant_selection.combinations[].route_slug
variant_selection.combinations[].route_href

route_href enthält beim Rebuild einen internen Prefix-Platzhalter. Die Site-Runtime ersetzt ihn bei der initialen Auslieferung durch den tatsächlichen dynamicRoute.prefix; die Shop-Runtime tut dasselbe für progressiv nachgeladene Snippets. Das Snippet muss den Prefix nicht kennen und darf keine URL aus der SKU bilden. Ist keine ProductRoute live, fehlen route_slug und route_href; die Variante kann trotzdem veröffentlicht und per variant_id verwendet werden.

Kann der Server keine eindeutige public-safe Matrix bilden, fehlt variant_selection vollständig. Das with im folgenden Beispiel rendert dann kein Auswahl-Control; die aktuell geladene Produktvariante bleibt unverändert. Leiten Sie keine Geschwistervarianten aus anderen Feldern des ViewModels ab.

Für eine einzelne Achse wie T-Shirt-Größe kann jede Kombination direkt als Control erscheinen:

html
{{ with $selection := index . "variant_selection" }}
  <div class="variant-sizes" aria-label="Größe">
    {{ range $combination := index $selection "combinations" }}
      {{ $size := index (index $combination "values") "size" }}
      {{ with $href := index $combination "route_href" }}
        <a href="{{ $href }}"
           data-nucleus-variant-choice
           data-variant-id="{{ index $combination "variant_id" }}"
           {{ if index $combination "active" }}aria-current="true"{{ end }}>{{ $size }}</a>
      {{ else }}
        <button type="button"
                data-nucleus-variant-choice
                data-variant-id="{{ index $combination "variant_id" }}"
                {{ if index $combination "active" }}aria-pressed="true"{{ end }}>{{ $size }}</button>
      {{ end }}
    {{ end }}
  </div>
{{ end }}

Das Site-Projekt bestimmt Elemente, Beschriftungen, Layout und Styles. Die Standard-Shop-Runtime erweitert nur Controls mit data-nucleus-variant-choice und data-variant-id. Sie lädt die öffentlichen Details und alle auf der Seite markierten ProductSnippet-Artefakte der Zielvariante, bereitet sämtliche Fragmente vor und ersetzt sie gemeinsam. Parallel dazu startet sie die Auflösung von Pricing und Availability für die neue Variante. Nach dem Fragmenttausch wartet sie dieses Ergebnis ab und sendet nucleus:product-variant-change am document.

Eine neuere Auswahl löst einen noch laufenden Wechsel ab. Nach dieser Ablösung darf der ältere Request weder Fragmente, URL oder Titel verändern noch ein Erfolgsereignis senden. Insbesondere verwirft die Runtime eine verspätete Pricing-Antwort des älteren Requests.

Das Erfolgsereignis übergibt den Laufzeitvertrag in event.detail:

js
document.addEventListener("nucleus:product-variant-change", (event) => {
  const { product, pricing, href } = event.detail;
  // Aktualisieren Sie hier Ihre dynamischen Preis- und Warenkorb-Controls.
});
  • product ist die öffentliche Produktdetailantwort der aktiven Variante.
  • pricing ist das erste Ergebnis von pricing/resolve einschließlich availabilityState. Schlägt nur diese Auflösung fehl, ist pricing gleich null.
  • href ist die echte same-origin-Ziel-URL oder bei einer route-losen Variante ein leerer String.

Die Runtime ersetzt die markierten ProductSnippet-Fragmente. Das Site-Frontend bleibt für weitere dynamische Preis-, Verfügbarkeits- und Warenkorb-Controls verantwortlich: Aktualisieren Sie diese Controls aus dem Ereignis und entfernen Sie veraltete Werte, wenn pricing gleich null ist. Verwenden Sie beim Add-to-Cart weiterhin nur die aktive product.variantId, Menge und öffentliche Einheit; Snippet-HTML ist keine Preis- oder Checkout-Autorität.

Bei einem fehlgeschlagenen Fragmentwechsel sendet die Runtime nucleus:product-variant-error mit event.detail.variantId und event.detail.code. Ein Frontend darf damit einen eigenen Fehlerzustand anzeigen, aber keine Zielvariante oder interne Ersatzroute ableiten.

Wählen Sie das Navigationsverhalten bewusst:

  • Lassen Sie die beiden Enhancement-Attribute weg oder setzen Sie data-nucleus-variant-navigation="page", wenn der Link die Seite immer vollständig neu laden soll.
  • Verwenden Sie echte Links mit Enhancement-Attributen, wenn die Runtime die Fragmente tauschen und die Browser-URL auf die echte Route aktualisieren soll. Ohne JavaScript funktioniert weiterhin der Link.
  • Verwenden Sie einen Button nur für eine route-lose Kombination. Der Fragmenttausch ändert dann die URL nicht und ist nicht als eigener Zustand teilbar.

Bei zwei Achsen können Sie jede gültige Kombination als konkretes Control rendern, zum Beispiel Rot / M und Rot / L. Eine zweistufige Farb- und Größenbedienung darf zusätzliche Frontend-Logik verwenden, muss aber immer eine vorhandene Kombination mit konkreter variant_id wählen. Erfinden oder kombinieren Sie keine Varianten-IDs aus Achsenwerten.

Die unsichtbaren ProductSnippet-Kommentargrenzen gehören zur Runtime. Legen Sie keine eigenen Wrapper oder Marker dafür an. Fehlt beim Ziel ein Required-Artefakt, bleibt der aktuelle DOM-Zustand vollständig erhalten. Bei einem echten Variantenlink navigiert der Browser anschließend regulär zur Zielroute; bei einer route-losen Variante meldet die Runtime nucleus:product-variant-error und lässt Seite sowie URL unverändert.

Storefront-Vertrag

Eine Storefront nutzt öffentliche Verträge:

  1. Sie lädt den Storefront-Kontext.
  2. Sie sucht Produkte über den Public Catalog.
  3. Sie lädt Product Summary oder Produktdetaildaten.
  4. Sie rendert Produktrelationen aus öffentlichen Recommendation-Gruppen, wenn die Detaildaten oder Snippet-Artefakte sie enthalten. Mehrere Produktbereiche auf derselben Seite bleiben dabei getrennte Bereiche mit eigener Einbindung und eigenem Leerzustand.
  5. Sie rendert Produktmedien aus media.<purposeKey>[]; für einfache Karten nutzt sie media.primary[0] oder den Alias primary_image_url.
  6. Sie löst Preise und Verfügbarkeit getrennt über Pricing auf.
  7. Sie bindet ein passendes Snippet-Artefakt nur ein, wenn Tenant, Variante, Sprache und Vertriebskanal zum aktuellen Kontext passen.
  8. Sie lässt optionale Snippet-Bereiche weg und behandelt fehlende Required-Snippets als temporär nicht bereite Detailseite.

Wenn kein passendes optionales Snippet vorliegt, rendert die Storefront aus öffentlichen Produktdaten und Pricing oder zeigt einen definierten Ladezustand.

Es gibt zwei Einbindungspfade:

PfadVerwendungVerhalten bei fehlendem Snippet
Dynamische Produktdetailseite im SitegeneratorDas Site-YAML nutzt type: productSnippet mit name und optional params.required: true. Workspace ersetzt den Marker serverseitig durch das Artefakt aus dem aktuellen Site-Scope.Required liefert 503 Service Unavailable mit Retry-After; optional rendert leer.
Headless Storefront-ClientDer Client lädt Produktdetaildaten über Public Catalog und fordert Snippet-Namen an. Die Antwort enthält Snippet-Referenzen mit öffentlichen URLs.Der Client behandelt fehlende optionale URLs oder 404 als leeren Darstellungsbereich und lädt Preise/Verfügbarkeit weiter getrennt.

Beispiel für eine dynamische Detailseite:

yaml
containers:
  mainContent:
    - type: productSnippet
      name: product-detail-hero
      params:
        required: true
    - type: productSnippet
      name: product-detail-content

Beispiel für eine headless Detailabfrage mit konkreten Snippet-Namen:

http
GET /api/v1/public/v1/catalog/products/by-route/produkte/beispielprodukt?snippets=product-detail-hero,product-detail-content

Snippet-URLs zeigen auf das gebaute Artefakt, zum Beispiel:

text
/assets/<tenant-id>/snippets/variant/<variant-id>/<snippet-name>/ch/<sales-channel-id>/loc/<locale>/index.html

Ungescopte Fallback-Artefakte liegen unter:

text
/assets/<tenant-id>/snippets/variant/<variant-id>/<snippet-name>.html

Die serverseitige Produktdetailseite prüft die Fallback-Reihenfolge selbst: Sales Channel und Locale, Sales Channel, Locale, global. Ein headless Client soll trotzdem den vom Public-Catalog-Vertrag gelieferten Snippet-URL verwenden und keine privaten Dateipfade oder interne Rebuild-Logik nachbauen.

Details zu Katalog, Pricing und Warenkorb finden Sie in Katalog, Preise und Produktdarstellung und Warenkorb, Checkout und Payment.

Erfolg erkennen

Ein Produkt-Snippet ist einsatzbereit, wenn diese Punkte erfüllt sind:

  • Die Snippet-Vorlage ist aktiv und hat resourceType=variant.
  • Der Name der Vorlage stimmt exakt mit dem erwarteten productSnippet.name oder dem angeforderten Snippet-Namen überein.
  • Der Preview-Tab rendert mit der echten Variante, Locale und salesChannelId ohne Fehler.
  • Das Preview-viewModel enthält die erwarteten Root-Felder oder content.<purposeKey>.fields.
  • Medienbereiche nutzen vereinbarte media.<purposeKey>[]-Gruppen und funktionieren auch, wenn optionale Gruppen fehlen.
  • Für jeden Ziel-Scope ist klar, ob Workspace eine kombinierte, channel-spezifische, sprachspezifische oder globale Vorlage verwendet.
  • Nach dem Rebuild ist das Artefakt über die öffentliche /assets/...-URL erreichbar.
  • Eine dynamische Detailseite ersetzt alle productSnippet-Marker.
  • Required-Snippets liefern keinen 503 mehr.
  • Preise, Verfügbarkeit, Cart und Checkout kommen weiterhin aus ihren öffentlichen Storefront-Verträgen.

Häufige Fehler

  • Eine Snippet-Vorlage enthält Sprache oder Vertriebskanal im Namen statt im Scope.
  • Eine globale Vorlage enthält feste sichtbare Texte, die nicht für alle Storefront-Sprachen passen.
  • Ein Team erwartet, dass ein globales Template fehlende Produktinhalte, ProductRoutes oder Sprachdaten ersetzt.
  • Ein tenantweiter Rebuild ohne Filter wird mit einem globalen Template verwechselt.
  • Eine Vorlage rendert leere Familienfelder als sichtbare Leerbereiche.
  • Eine Vorlage übersetzt technische Relationstypen statt nur sichtbare Überschriften.
  • Ein Transform berechnet Preise oder Verfügbarkeit.
  • Eine Storefront behandelt Snippets als Datenquelle für Checkout-Entscheidungen.
  • Nach Template- oder Inhaltsänderungen wird kein Rebuild gestartet.
  • Ein Template nutzt Punktpfade auf optionale Zwecke oder Felder und bricht bei fehlenden Daten mit einem Template-Fehler ab.
  • Ein Template entscheidet anhand von Dateinamen, Dateiendungen oder Upload-Reihenfolge, ob ein Asset Hero, Galerie, Detail, Download oder Video ist.
  • Ein Frontend-Team benennt Zweck-Keys lokal um, ohne den zentralen PIM-Medienzweck und die Snippet-Vorlage gemeinsam anzupassen.
  • Preview und Storefront verwenden unterschiedliche Locale- oder Sales-Channel-Werte.
  • Ein headless Client baut Snippet-URLs selbst zusammen, obwohl die Detailantwort bereits Snippet-Referenzen liefern kann.
  • Ein Required-Snippet wird gesetzt, obwohl der Bereich fachlich optional ist.