SEO: Paginierung für Onlineshops und inhaltsreiche Seiten

Paginierung mit rel="next" und rel="prev"

Onlineshop-Betreiber kennen das Problem: Produktkategorieseiten beinhalten oftmals viele Produkte, die über mehrere Seiten verteilt dargestellt werden.

UPDATE: Google hat bekannt gegeben, dass die Auszeichnung mit rel=“prev“ und rel=“next“ nunmehr kein Indexierungssignal darstellt.

Ohne weiteres Zutun indexieren die Suchmaschinen diese Seiten separat und erkennen hier doppelten Inhalt (“duplicate content”).
Mit ein paar Handgriffen zeigen wir, wie dieses Problem vermieden werden kann.

Alles auf einer Seite: Bietet sich eine Gesamtansicht an?

Zuallererst sollten Sie sich die Frage stellen, ob Sie Ihren Besuchern eine “view-all”-Seite anbieten möchten, d. h. alle Produkte – oder besser: alle Elemente, die normalerweise über mehrere Seiten verteilt dargestellt werden – finden Sie dann auf einer Gesamtseite wieder.

Paginierung Gesamtseite

Vorteil: Die paginierten Seiten 1 bis n verweisen über den sogenannten canonical-Tag auf die Seite, auf der die Inhalte zusammengeführt werden. Über einen canonical-Tag teilen Sie den Suchmaschinen mit, wie diese mit (weitgehend) identischen Inhalten verschiedener Seiten umgehen soll. Durch die Angabe verweisen Sie auf eine Orginalressource bzw. URL .

Nachteil: Was bei einem mehrere Seiten langen News-Artikel, der vorzugsweise aus textlichem Inhalt besteht, charmant klingt, stößt bei der Produktkategorieseite eines Onlineshops mit hunderten Artikeln samt Produktbildern auf das Problem langer Ladezeiten. Da Latenzen ein Rankingfaktor sind, hört bei Google der Spaß bei Ladezeiten jenseits von drei Sekunden schnell auf.

Ohne Gesamtansicht: Inhalte auf mehrere Seiten verteilt

In solchen Fällen ist eine “view-all”-Page nicht sinnvoll. Hier empfiehlt es sich, alle Folgeseiten, also die Seiten 2 bis n (n=letzte Seite) als ergänzende oder weiterführende Seiten zu kennzeichnen. Sie teilen der Suchmaschine mit, dass die Seiten 1 bis n zusammengehören, also im Grunde als „Kapitel“ in das Inhaltsverzeichnis aufgenommen werden sollen.

Paginierung mit rel="next" und rel="prev"

Die Auszeichnungen erfolgen im <head> Bereich des Markup. Auf der ersten Seite teilen Sie der Suchmaschine über ein <link> Element mit, dass es weitere, ergänzende Seiten gibt:
<link rel=”next” href=”seite-2.html”>

Auf der zweiten bis vorletzten Seite (Kompontentenseiten) definieren Sie die vorige und die nachfolgende Seite:
<link rel=”prev” href=”seite-1.html”>
<link rel=”next” href=”seite-3.html”>

Auf der letzten Seite reicht der Hinweis auf die vorige Seite (eine nächste Seite gibt es ja nicht):
<link rel=”prev” href=”seite-n-1.html”>

Ergänzend soll verhindert werden, dass die Seiten 2 bis n in den Index der Suchmaschinen aufgenommen werden (die Linkkraft der Produkte hingegen soll behalten werden). Dazu bedienen Sie sich folgenden Elements:
<meta name=”robots” content=”noindex, follow”>

Bitte achten Sie darauf, dass die erste Seite ganz regulär indexiert werden darf:
<meta name=”robots” content=”index, follow”>

Zusätzlich zu rel=”prev” und rel=”next” lässt beispielsweise Zalando die Komponentenseiten (Folgeseiten) via canonical-Tag auf die kanonische URL zeigen. Das sieht dann in etwa so aus:
<link rel="canonical" href="seite-1">
Im href-Attribut sind übrigens relative und absolute Pfadangaben erlaubt.

Viele Wege führen nach Rom

Wie oftmals im Online-Marketing gibt es auch in Sachen Paginierung unterschiedliche Herangehensweisen.
So verzichten einige Online Shops auf den Einsatz von rel=”prev” und rel=”next” und verweigern den Suchmaschinen über die Meta-Robots die Indexierung der Komponentenseiten.

Welche Variante Sie auch bevorzugen: Das Hauptaugenmerk sollte darauf liegen, doppelten Inhalt zu vermeiden. Ob Sie dies nun mit einer „view all“ Seite oder über die Angaben im <head> Bereich Ihrer Seite lösen, hängt nicht zuletzt auch vom Kontext ab: Verteilen sich vorwiegend textliche Inhalte über mehrere Seiten, kann eine Gesamtseite – auch unter Usabilty-Gesichtspunkten – ideal sein. Bei bildlastigen Produktkategorieseiten sind rel="prev" und rel="next" sicherlich die bessere Wahl.

Hier sei noch angemerkt, dass nicht sicher ist, ob andere Suchmaschinen wie bpsw. Bing die Attributswerte „next“ und „prev“ in gleicher Weise interpretieren wie Google. Google selbst hat bereits im September 2011 erklärt, die Paginierung von Seiten mit rel=“next“ und rel=“prev“ zu unterstüzen.

E-Spirit Usergroup: Perfekter Auftakt im Unperfekthaus

ESUG-Gründungsveranstaltung

Unter dem Motto “Real-Life Community für FirstSpirit-Anwender” nahmen rund 40 Teilnehmer an der Auftaktveranstaltung zur Gründung der e-Spirit Usergroup – kurz ESUG – teil.
Das Unperfekthaus in Essen ist Anlaufstelle für Kreative, Co-Worker und bietet neben den Seminarräumen auch Ausstellungsflächen, eine Bühne, eine riesige Dachterrasse und mehrere “WG-Zimmer”.

Aufgrund des zahlreichen und positiven Feedbacks war der Wunsch einer Community für Anwender, Nutzer und Dienstleister schon im Vorfeld der Planungen spürbar gewesen.

Die Teilnehmer-Erwartungen reichten vom konkurrenzfreien Netzwerken, dem Austausch von Erfahrungen und Best-Practices bis hin zum konkreten Vorantreiben von speziellen Anforderungen/ Optimierungen.

Was ist also der Kerngedanke der ESUG?

Die Usergroup möchte den Anwendern, Kunden und Partnern eine unabhängige Plattform bieten, um gegenüber e-Spirit als Hersteller eine Interessenvertretung zu bilden. Durch intensiven Austausch von Wissen und Erfahrungen werden außerdem Synergien zwischen den Partnern geschaffen.

ESUG Gründungsveranstaltung

Die Gründungsveranstaltung

Die Veranstaltung begann mit einem Meet&Greet für die Dienstleister, Freiberufler, Agenturen, Modulhersteller und Kunden. So konnten die Teilnehmer direkt neue Kontakte knüpfen, bestehende auffrischen und vorab die ersten informellen Gespräche führen.

Jörn Bodemann ließ es sich als CEO der e-Spirit AG nicht nehmen, dem gesamten Projekt seine Unterstützung zuzusichern und kündigte den bevorstehenden e-Spirit Partnertag im Phantasialand Brühl an.

Michael Bartz von der USU AG veranschaulichte, wie man Herausforderungen üblicher Anwender-Szenarien durch die Vereinigung der Stärken von CMS und Portalen am Beispiel von Liferay begegnen kann.

Warum gründet man eine Usergroup?

Es gibt viele Gründe sich in einer Interessengruppe zu organiseren – die ausschlaggebendsten sind wohl die gleichen Ziele:

  • Steigerung der Kundenorientierung und -zufriedenheit.
  • Alle erhalten gemeinsam eine gewichtige Stimme gegenüber dem Hersteller.
  • Gemeinsam kann man voneinander lernen und Erfahrungen aus erster Hand austauschen.
  • Miteinander kann man die gängigen Painpoints beim Hersteller anmerken.
  • Zusammen können eigene Module, Features und Lösungen erarbeitet werden.
  • Eine Verbesserung der Entwickler- und Anwenderorientierung.

Außerdem soll über Kooperationen der Nachwuchs aktiv gefordert und gefördert werden und damit auch ein wichtiger, gemeinnütziger Beitrag geleistet werden.

Vortrag von Andreas Kämmer, comspace bei der ESUG Gründungsveranstaltung

Vereinsgründung mit (e-)Spirit

Nach der Mittagspause ging es im zweiten Teil um die Gründung des Vereins mit allen organisatorischen Facetten:

  • Welche Voraussetzungen sollen künftige Mitglieder mitbringen?
  • Wie kann sich der Verein seine Hersteller-Unabhängigkeit bewahren?
  • Was soll zukünftig Inhalt kommender Veranstaltungen sein?
  • Welche Veranstaltungsarten, wie z.B. Roundtables, Barcamps, Workhops sind seitens der Mitglieder gewünscht?
  • Wie soll der fachliche Austausch stattfinden
  • Wie werden gemeinsam erarbeitete Produkte, Projekte, Konzepte & Techniken zukünftig gehandhabt?

Die beschlossenen Details zur Usergroup, können in Kürze in der aktuellen Satzung eingesehen werden.

Der Vorstand:

Im Rahmen der ersten Gründungssitzung wurde der Verein gegen 17.05 Uhr gegründet und der Vorstand wie folgt gewählt:
1. Vorsitzender – Andreas Kämmer, comspace GmbH & Co.KG
2. Vorsitzender – Dirk Langenheim, Intentive GmbH
3. Vorsitzender – Andreas Gromadecki, Pinuts media + science GmbH

Zukunftsausblick

Als erste, kurzfristige Maßnahmen wurden beschlossen die Mitgliederwerbung und die thematische Schwerpunktsetzung für die kommenden Veranstaltungen in den Vordergrund zu stellen. Daneben wurde die erste Kooperation mit dem anstehenden e-Spirit Barcamp angeregt, die Bildung von technisch ausgerichteten Arbeitskreisen oder Kooperationen mit Hochschulen.

Die Erwartungen an kommende Veranstaltungen sind hoch. So könnten Teilnehmer Ihre eigenen Erfahrungen in Form von Usecases und Workarounds teilen, wie es hier zum Beispiel Arithnea und Publicis Pixelpark vormachen.
Wer sich nun von den Vorteilen der Usergroup angesprochen fühlt und mitmachen möchte, kann sich auf www.e-spirit-usergroup.de gleich anmelden oder auf Twitter unter dem Hashtag #esug auf dem Laufenden bleiben.

Sitecore Commerce Connect – Teil 5: Warenkorb verwalten

Bisher ging es in dieser Serie um die Verwaltung und Darstellung von Produkten und Preisen aus Hybris. Das Ziel dieses Artikels ist es auch erste Prozesse rund um den Warenkorb mittels Sitecore Commerce Connect abbilden zu können.

Warenkorb

Die zentrale Überlegung am Anfang eines Projektes ist, welches System zu welcher Zeit die Zuständigkeit für den Warenkorb hat. Wird der Warenkorb immer auf beiden Seiten synchron gehalten, das heißt jede Änderung am Warenkorb wird unmittelbar an das externe Shopsystem (kurz ECS) gemeldet, oder wird der Warenkorb im ECS nur zu definierten Zeitpunkten synchronisiert, also z.B. beim Checkout? Letzteres bedingt weniger Requests an das ECS, aber Sitecore-seitig ist dann z.B. eine Warenkorbwertermittlung erforderlich, die im schlechtesten Fall nicht mit der im ECS synchron ist.

Warenkorb-Verwaltung

Im Rahmen unserer Evaluierung ist das ECS für den Warenkorb zuständig und Sitecore meldet alle Änderungen unmittelbar an das ECS. Für die Darstellung auf der Webseite werden die Daten rund um den Warenkorb wiederum immer online vom ECS abgefragt.
Außerdem haben wir uns entschieden, den mitgelieferten Engagement Plan ‘Abandoned Carts’ gleich mit zu nutzen, zumal dafür scheinbar keine weitere Implementierung erforderlich ist.

Warenkorb anlegen/ abfragen

Für alle Funktionen rund um den Warenkorb ist der Service Provider ‚CartServiceProvider‘ des Cart Service Layers zuständig.

Beispiel Verwendung CartServiceProvider.CreateOrResumeCart

[csharp]
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Comspace.Sitecore.CommerceConnect.Services.Prices;
using Sitecore;
using Sitecore.Analytics;
using Sitecore.Commerce.Entities.Carts;
using Sitecore.Commerce.Services.Carts;
using Sitecore.Commerce.Services.Prices;
using Sitecore.Data;

namespace Comspace.Sitecore.CommerceConnect.Services.Carts
{
/// <summary>
/// Sitecore.Commerce.Carts.config
/// </summary>
public class CartManager
{
public const string TrackerAttachmentKeyCart = "Comspace.Sitecore.CommerceConnect_Cart";

public static Cart GetCart()
{
if (!Tracker.Current.Contact.Attachments.ContainsKey(TrackerAttachmentKeyCart))
{
//create cart
var createOrResumeCartRequest = new CreateOrResumeCartRequest(
"Website",
Context.User.Profile.Email,
"Default",
Context.User.Profile.Email); //NOTE FindCartInEaState: c.CustomerId == request.CustomerId
var cart = new CartServiceProvider().CreateOrResumeCart(createOrResumeCartRequest).Cart;

//persist cart
Tracker.Current.Contact.Attachments[TrackerAttachmentKeyCart] = cart;
}
return Tracker.Current.Contact.Attachments[TrackerAttachmentKeyCart] as Cart;
}
}

//…
}
[/csharp]

Pipeline loadCart

Die Anbindung des ECS erfolgt mittels Erweiterung der Pipeline ‚loadCart‘ der Sitecore.Commerce.Carts.config.
.config

[xml]
<!– LOAD CART
Gets the cart object that matches the specified criteria.
This pipeline is responsible for reading data for a specific cart that is managed by the commerce system.
This pipeline reads the cart data from the commerce system and converts that data into the Commerce format.
–>
<commerce.carts.loadCart>
<processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Carts.LoadCart, Comspace.Sitecore.CommerceConnect.Hybris" patch:after="processor[@type=’Sitecore.Commerce.Pipelines.Carts.LoadCart.LoadCartFromEaState, Sitecore.Commerce‘]"/>
</commerce.carts.loadCart>
[/xml]

Processor LoadCart

[csharp]
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector.Model.Cart;
using Comspace.Sitecore.CommerceConnect.Services.Carts;
using Sitecore.Commerce.Entities.Carts;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Pipelines.Carts.Common;
using Sitecore.Commerce.Services.Carts;

namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Carts
{
/// <summary>
/// commerce.carts.loadCart
/// </summary>
public class LoadCart : CartPipelineProcessor
{
public override void Process(ServicePipelineArgs args)
{
var loadCartRequest = args.Request as LoadCartRequest;

//get from ECS
ExternalCart externalCart = CartConnector.Load(global::Sitecore.Context.Language.Name, loadCartRequest.UserId);

//convert to commerce connect cart
Cart cart = new Cart();
cart.ExternalId = externalCart.Code;
cart.ShopName = loadCartRequest.ShopName;
cart.UserId = loadCartRequest.UserId;

//init totals
cart = CartManager.UpdateCartTotals(cart); //NOTE prevent multiple request

//persist for next processor
((CartResult) args.Result).Cart = cart;
}
}
}
[/csharp]

Produkt zum Warenkorb hinzufügen

Beispiel Verwendung CartServiceProvider.AddCartLines

[csharp]
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Comspace.Sitecore.CommerceConnect.Services.Prices;
using Sitecore;
using Sitecore.Analytics;
using Sitecore.Commerce.Entities.Carts;
using Sitecore.Commerce.Services.Carts;
using Sitecore.Commerce.Services.Prices;
using Sitecore.Data;

namespace Comspace.Sitecore.CommerceConnect.Services.Carts
{
/// <summary>
/// Sitecore.Commerce.Carts.config
/// </summary>
public class CartManager
{
//…

public static Cart AddLine(string externalId, string sitecoreItemId, uint quantitiy)
{
Cart cart = GetCart();

//create cartlines
List<CartLine> lines = new List<CartLine>
{
new CartLine
{
Product = new CartProduct
{
ProductId = externalId,
SitecoreProductItemId = new ID(sitecoreItemId),
Price = PriceManager.GetPrice(externalId, Context.Language)
},
Quantity = quantitiy,
LineNumber = (uint) (cart.Lines.Count + 1)
}
};

//add cart lines
AddCartLinesRequest addCartLinerequest = new AddCartLinesRequest(cart, new ReadOnlyCollection<CartLine>(lines));
CartResult cartResult = new CartServiceProvider().AddCartLines(addCartLinerequest);

return cartResult.Cart; //result contains the updated cart
}
}
}
[/csharp]

Pipeline addCartLines

.config

[xml]
<!– ADD CART LINES
This pipeline is responsible for adding a new line to the shopping cart and recording a corresponding page event in DMS.
This happens when a product is added to the cart.
–>
<commerce.carts.addCartLines>
<processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Carts.AddLineToCart, Comspace.Sitecore.CommerceConnect.Hybris" patch:after="processor[@type=’Sitecore.Commerce.Pipelines.Carts.AddCartLines.AddLinesToCart, Sitecore.Commerce‘]"/>
</commerce.carts.addCartLines>
[/xml]

Processor AddCartLines

[csharp]
using System.Linq;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Sitecore.Commerce.Entities.Carts;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Carts;

namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Carts
{
/// <summary>
/// commerce.carts.addCartLines
/// </summary>
public class AddLineToCart : PipelineProcessor<ServicePipelineArgs>
{
public override void Process(ServicePipelineArgs args)
{
var cartLinesRequest = args.Request as CartLinesRequest;

//add to ECS
Cart cart = cartLinesRequest.Cart;
CartLine cartline = cartLinesRequest.Lines.First();
CartConnector.AddLineToCard(global::Sitecore.Context.Language.Name, cart, cartline);
}
}
}
[/csharp]

Warenkorb Summen

Damit die Summen im Warenkorb immer korrekt sind und keine Sitecore-seitige Berechnung stattfinden muss, werden auch die Warenkorb Summen immer online aus dem ECS geholt. Dafür muss die Pipeline ‘saveCart’ erweitert werden:

Pipeline saveCart

.config

[xml]
<!– SAVE CART
Saves the cart object to the external system and in Sitecore EA state.
–>
<commerce.carts.saveCart>
<processor type="Comspace.Sitecore.CommerceConnect.Pipelines.Carts.UpdateCartTotals, Comspace.Sitecore.CommerceConnect" patch:after="processor[@type=’Sitecore.Commerce.Pipelines.Carts.Common.SaveCartToEaState, Sitecore.Commerce‘]"/>
</commerce.carts.saveCart>
[/xml]

Processor UpdateCartTotals

[csharp]
using Comspace.Sitecore.CommerceConnect.Services.Carts;
using Sitecore.Commerce.Entities.Carts;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Carts;

namespace Comspace.Sitecore.CommerceConnect.Pipelines.Carts
{
/// <summary>
/// commerce.carts.saveCart
/// </summary>
public class UpdateCartTotals : PipelineProcessor<ServicePipelineArgs>
{
public override void Process(ServicePipelineArgs args)
{
var cartRequest = (CartRequestWithCart) args.Request;

//get from ECS
Cart cart = CartManager.UpdateCartTotals(cartRequest.Cart);

//persist for next processor
((CartResult) args.Result).Cart = cart;
}
}
}
[/csharp]

Beispiel Verwendung PricingServiceProvider.GetCartTotal

[csharp]
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Comspace.Sitecore.CommerceConnect.Services.Prices;
using Sitecore;
using Sitecore.Analytics;
using Sitecore.Commerce.Entities.Carts;
using Sitecore.Commerce.Services.Carts;
using Sitecore.Commerce.Services.Prices;
using Sitecore.Data;

namespace Comspace.Sitecore.CommerceConnect.Services.Carts
{
/// <summary>
/// Sitecore.Commerce.Carts.config
/// </summary>
public class CartManager
{
//…

public static Cart UpdateCartTotals(Cart cart)
{
var request = new GetCartTotalRequest();
request.Cart = cart;

//get from ECS
var cartTotalResult = new PricingServiceProvider().GetCartTotal(request); //result contains the updated cart

Tracker.Current.Contact.Attachments[TrackerAttachmentKeyCart] = cartTotalResult.Cart; //refresh cache
return cartTotalResult.Cart;
}
}
}
[/csharp]

Stolpersteine

Der Processor ‘FindCartInEaState’ benötigt eine CustomerId, damit der Contact ermittelt werden kann. Dafür muss die eigentlich optionale CustomerId mit an den ‘CreateOrResumeCartRequest’ übergeben werden (siehe oben). Anderenfalls wird immer wieder ein neuer Warenkorb angelegt, obwohl für den Contact bereits einer existiert.

Resumé

Und auch diesen letzten Beitrag möchte ich mit ein paar Eindrücken und Erkenntnissen abschließen:

  • Der Cart Sercice Layer ist sehr umfangreich und im Rahmen der Evaluierung konnte nur ein sehr kleiner Ausschnitt evaluiert werden.
  • Eine Herausforderung in einem echten Projekt sind die Zuständigkeit und Synchronisation zwischen dem Sitecore Shop und dem ECS. Was wird wo zwischengespeichert und wann an wen gemeldet?
  • Wie auch schon unter Stolpersteine beschrieben, war der Zugriff auf die Contacts im Engangement Plan (automatisch im Hintergrund) immer wieder holprig. Eine der Ursachen war sicherlich die Entwicklungsumgebung: wechselnde Browser, plötzliche Neustarts etc.  Hier müsste in einem echten Projekt eine kritische Analyse erfolgen.
  • Auch dem Umgang mit Fehlern in der Verarbeitung muss in einem echten Projekt eine hohe Aufmerksamkeit gewidmet werden – Stichwort Transaktionssicherheit.

So, das war’s erstmal von meiner Seite zum Thema Commerce Connect. Ich freue mich über euer Feedback 🙂

Alle voran gegangenen Artikel dieser Serie findet ihr hier:
Teil 1: Ein Überblick
Teil 2: Produkte synchronisieren
Teil 3: Klassifizierungen synchronisieren
Teil 4: Preise darstellen

Sitecore Commerce Connect – Teil 3: Klassifizierungen synchronisieren

Im letzten Teil dieser Serie ging es um die Synchronisation von Produkten. Das Ziel dieses Blog-Artikels ist es auch Klassifizierungen unidirektional aus dem Sitecore heraus mit dem externen Commerce System (kurz ECS, in unserem Fall Hybris) zu synchronisieren und den Produkten zuzuordnen.

Klassifizierungen sind im Projekt individuell definierbare Spezifikationen zur Beschreibung von Produkten. Die sog. Classifications in Sitecore können über eine Parent-Verknüpfung (ExternalParentID) beliebig geschachtelt werden, die erste Ebene ist aber immer eine Gruppe, eine sog. ClassificationGroup.
ClassificationRepo

Synchronisation Klassifizierungen

Um Klassifizierungen zu synchronisieren muss zunächst die Pipeline ’synchronizeClassifications‘ der Sitecore.Commerce.Products.config erweitert werden:

Pipeline synchronizeClassifications

.config

[xml]
<!– SYNCHRONIZE CLASSIFICATIONS –>
<commerce.synchronizeProducts.synchronizeClassifications>
<processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ReadSitecoreClassifications, Sitecore.Commerce">
<patch:delete />
</processor>
<processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ResolveClassificationsChanges, Sitecore.Commerce">
<patch:delete />
</processor>
<processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.SaveClassificationsToExternalCommerceSystem, Sitecore.Commerce" >
<patch:delete />
</processor>
<processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Classifications.ReadClassifications, Comspace.Sitecore.CommerceConnect.Hybris" patch:after="processor[@type=’Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ReadExternalCommerceSystemClassifications, Sitecore.Commerce‘]" />
<processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Classifications.ReadClassificationGroups, Comspace.Sitecore.CommerceConnect.Hybris" patch:after="processor[@type=’Sitecore.Commerce.Pipelines.Products.SynchronizeClassifications.ReadExternalCommerceSystemClassifications, Sitecore.Commerce‘]" />
</commerce.synchronizeProducts.synchronizeClassifications>
[/xml]

Processor ReadClassificationGroups

[csharp]using System.Collections.Generic;
using System.Linq;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector.Model;
using Sitecore.Commerce.Entities.Products;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Products;

namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Classifications
{
/// <summary>
/// commerce.synchronizeProducts.synchronizeClassifications
/// </summary>
public class ReadClassificationGroups : PipelineProcessor<ServicePipelineArgs>
{
public override void Process(ServicePipelineArgs args)
{
var request = args.Request as SynchronizationRequest;
var groups = request.Properties["ClassificationGroups"] as List<ClassificationGroup>; //integration guide (page 9)
groups = groups ?? new List<ClassificationGroup>();

//get groups from ECS
IEnumerable<ExternalCategoryGroup> categoryGroups = ClassificationConnector.Load(request.Language);

//convert and add to commerce connect list
groups.AddRange(from @group in categoryGroups
select new ClassificationGroup
{
ExternalId = @group.Code,
Name = @group.Name
});

//persist for next processor
request.Properties["ClassificationGroups"] = groups;
}
}
}[/csharp]

Processor ReadClassifications

[csharp]using System.Collections.Generic;
using System.Collections.ObjectModel;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector.Model;
using Sitecore.Commerce.Entities.Products;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Products;

namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Classifications
{
/// <summary>
/// commerce.synchronizeProducts.synchronizeClassifications
/// </summary>
public class ReadClassifications : PipelineProcessor<ServicePipelineArgs>
{
public override void Process(ServicePipelineArgs args)
{
var request = args.Request as SynchronizationRequest;
var groups = request.Properties["ClassificationGroups"] as List<ClassificationGroup>; ////integration guide (page 9)
groups = groups != null ? groups : new List<ClassificationGroup>();

foreach (ClassificationGroup group in groups)
{
var classifications = new List<Classification>();

//get categories from ECS
foreach (ExternalCategory externalCategory in ClassificationConnector.Load(group.ExternalId, request.Language)) //TBD
{
//convert to commerce connect classification
var classification = new Classification();
classification.ExternalId = externalCategory.Code;
classification.Name = externalCategory.Name;
classifications.Add(classification);

//…with sub-classifications
HandleSubCategories(externalCategory, classifications);
}
group.Classifications = new ReadOnlyCollection<Classification>(classifications);
}

//persist for next processor
request.Properties["ClassificationGroups"] = groups;
}

private static void HandleSubCategories(ExternalCategory parentCategory, List<Classification> allClassifications)
{
if (parentCategory.Categories != null)
{
foreach (var category in parentCategory.Categories)
{
var classification = new Classification();
classification.ExternalId = category.Code;
classification.Name = category.Name;
classification.ExternalParentId = parentCategory.Code;

allClassifications.Add(classification);
HandleSubCategories(category, allClassifications);
}
}
}
}
}
[/csharp]

Klassifizierungen Produkten zuordnen

Anschließend müssen die Klassifizierungen nur noch den Produkten zugeordnet werden. Und auch dafür muss wieder eine Pipeline der Sitecore.Commerce.Products.config erweitert werden:

Pipeline synchronizeProductClassifications

.config

[xml]<!– SYNCHRONIZE PRODUCT CLASSIFICATIONS
This pipeline is responsible for synchronizing and updating the references between a given product and associated classifications and categories within.
It’s assumed that classifications are already synchronized and present in CMS.
The references to categories are stored directly on the main product item. –>
<commerce.synchronizeProducts.synchronizeProductClassifications>
<processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products.ReadProductClassifications, Comspace.Sitecore.CommerceConnect.Hybris" patch:after="processor[@type=’Sitecore.Commerce.Pipelines.Products.SynchronizeProductClassifications.ReadExternalCommerceSystemProductClassifications, Sitecore.Commerce‘]" />
</commerce.synchronizeProducts.synchronizeProductClassifications>[/xml]

Processor ReadProductClassifications

[csharp]using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector.Model;
using Sitecore.Commerce.Entities.Products;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Products;

namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products
{
/// <summary>
/// commerce.synchronizeProducts.synchronizeProductClassifications
/// </summary>
public class ReadProductClassifications : ReadProduct
{
public override void Process(ServicePipelineArgs args)
{
var request = args.Request as SynchronizeProductRequest;
var product = request.Properties["Product"] as Product;
var groups = request.Properties["ClassificationGroups"] as List<ClassificationGroup>;
groups = groups != null ? groups : new List<ClassificationGroup>();

//get product from ECS
ExternalProduct externalProduct = ProductConnector.Load(request.Language, product.ExternalId); //tbd
if (externalProduct != null && IsProductValidForImport(externalProduct))
{
//convert to commerce connect product classification groups and classifications
foreach (ExternalCategoryGroup externalCategoryGroup in externalProduct.CategoryGroups)
{
ClassificationGroup group = new ClassificationGroup();
group.ExternalId = externalCategoryGroup.Code;
group.Name = externalCategoryGroup.Name;

IEnumerable<Classification> classifications = from externalCategory in externalCategoryGroup.Categories
select new Classification
{
ExternalId = externalCategory.Code,
Name = externalCategory.Name
};
group.Classifications = new ReadOnlyCollection<Classification>(classifications.ToList());

groups.Add(group);
}
}

//persist for next processor
request.Properties["ClassificationGroups"] = groups;
}
}
}[/csharp]

Resumé

Mein erster Eindruck:

  • Es gibt viel Dokumentation, aber z.B. zum Thema Zuordnung von Klassifikationen zu Produkten konnte ich nichts finden. Da habt ihr es jetzt ja besser 😉 Häufig war dann doch dotPeek mein bester Freund.
  • Der Differenzabgleich funktionierte problemlos, Mehrsprachigkeit war im Kontext der Zuordnung von Klassifizierungen zu Produkten noch etwas holprig, da sollte ein zweiter Blick erfolgen.

Wenn ihr mehr zum Sitecore Commerce Connect wissen wollt, dann schaut mal hier:

Sitecore Commerce Connect – Teil 2: Produkte synchronisieren

Im ersten Teil dieser Serie ging es darum einen kurzen Überblick über das Sitecore Commerce Connect Modul zu verschaffen. In diesem Beitrag geht es nun um die Synchronisation von Produkten zwischen einem externen Shopsystem (kurz ECS, in unserem PoC Hybris) und Sitecore.

Zunächst ist es wichtig zu verstehen, dass Preise und Bestand im Commerce Connect nicht als Produktstammdaten gelten. Der Bestand ändert sich ständig und DEN Preis gibt es ja auch nicht, Preisfindung ist ein sehr komplexes Thema. Sitecore geht davon aus, dass diese Informationen online vom ECS ermittelt und geliefert werden. Im Teil 4 dieser Serie wird dann beschrieben, wie ihr auch an diese Informationen kommt.

image00

Die Hoheit für die Produktdaten liegt sicherlich im ECS. Für die Synchronisation der Produktdaten schlägt Sitecore folgende Varianten vor:

  1. Produkte als Items in Sitecore Datenbank (optional bidirektional)
  2. Produkte in ECS, Zugriff über Sitecore Data Provider
  3. Produkte in ECS, Zugriff über Sitecore Index

Wir haben uns im Verlauf des PoC für die Variante 1 entschieden, da diese standardmäßig vom Sitecore so vorgesehen ist und den höchsten Komfort bietet. In der Sitecore Commerce Components Overview im Kapitel “Products” sind einige Argumente für oder gegen die unterschiedlichen Variante aufgeführt.

So, jetzt aber Butter bei die Fische 🙂

Installation Modul

Falls ihr das Modul noch nicht installiert habt, findet ihr wie gewohnt eine gute Installationsanleitung auf dev.sitecore.net.

Synchronisation Produkte

Das Ziel: Titel, Beschreibung und Bilder der Produkte aus dem externen Shopsystem mit Sitecore zu synchronisieren.

image01

In folgende Pipelines der Sitecore.Commerce.Products.config muss für die unidirektionale Synchronisation der Produkte eingegriffen werden:

Pipeline getExternalCommerceSystemProductList

.config

[xml]
&lt;!– GET EXTERNAL COMMERCE SYSTEM PRODUCT LIST
This pipeline is responsible for obtaining the list of product Ids to be synchronized from the external commerce system.–&gt;
&lt;commerce.synchronizeProducts.getExternalCommerceSystemProductList&gt;
&lt;processor type="Sitecore.Commerce.Pipelines.Products.GetExternalCommerceSystemProductList.GetExternalCommerceSystemProductList, Sitecore.Commerce"&gt;
&lt;patch:delete /&gt;
&lt;/processor&gt;
&lt;processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products.ReadProducts, Comspace.Sitecore.CommerceConnect.Hybris" /&gt;
&lt;/commerce.synchronizeProducts.getExternalCommerceSystemProductList&gt;
[/xml]

Processor ReadProducts

[csharp]
using System.Collections.Generic;
using System.Linq;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector.Model;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Products;

namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products
{
/// &lt;summary&gt;
/// commerce.synchronizeProducts.getExternalCommerceSystemProductList
/// &lt;/summary&gt;
public class ReadProducts : PipelineProcessor&lt;ServicePipelineArgs&gt;
{
public override void Process(ServicePipelineArgs args)
{
var request = args.Request as SynchronizeProductsRequest;
var productIds = args.Request.Properties["ExternalCommerceSystemProductIds"] as List&lt;string&gt;; //integration guide (page 12)
productIds = productIds ?? new List&lt;string&gt;();

//get products from ECS
IEnumerable&lt;ExternalProduct&gt; externalProducts = ProductConnector.Load(request.Language);

//convert and add to commerce connect list
productIds.AddRange(from product in externalProducts
where product.Code != null
select product.Code);

//persist for next processor
args.Request.Properties["ExternalCommerceSystemProductIds"] = productIds;
}
}
}
[/csharp]

Pipeline synchronizeProductEntity

.config

[xml]
&lt;!– SYNCHRONIZE PRODUCT ITEM –&gt;
&lt;commerce.synchronizeProducts.synchronizeProductEntity&gt;
&lt;processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeProductEntity.ReadProductFromSitecore, Sitecore.Commerce"&gt;
&lt;patch:delete /&gt;
&lt;/processor&gt;
&lt;processor type="Sitecore.Commerce.Pipelines.Products.SynchronizeProductEntity.SaveProductToExternalCommerceSystem, Sitecore.Commerce" &gt;
&lt;patch:delete /&gt;
&lt;/processor&gt;
&lt;processor type="Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products.ReadProduct, Comspace.Sitecore.CommerceConnect.Hybris" patch:after="processor[@type=’Sitecore.Commerce.Pipelines.Products.SynchronizeProductEntity.ReadExternalCommerceSystemProduct, Sitecore.Commerce‘]" /&gt;
&lt;/commerce.synchronizeProducts.synchronizeProductEntity&gt;
[/xml]

Processor ReadProduct

[csharp]
using Comspace.Sitecore.CommerceConnect.Entities.Products;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector;
using Comspace.Sitecore.CommerceConnect.Hybris.Connector.Model;
using Sitecore.Commerce.Pipelines;
using Sitecore.Commerce.Services.Products;
using Sitecore.Diagnostics;

namespace Comspace.Sitecore.CommerceConnect.Hybris.Pipelines.Products
{
/// &lt;summary&gt;
/// commerce.synchronizeProducts.synchronizeProductEntity
/// &lt;/summary&gt;
public class ReadProduct : PipelineProcessor&lt;ServicePipelineArgs&gt;
{
public override void Process(ServicePipelineArgs args)
{
SynchronizeProductRequest syncProdRequest = args.Request as SynchronizeProductRequest;

//get product from ECS
ExternalProduct externalProduct = ProductConnector.Load(syncProdRequest.Language, syncProdRequest.ProductId); //TBD
if (externalProduct != null &amp;&amp; IsProductValidForImport(externalProduct))
{
CustomProduct product = new CustomProduct();
product.ExternalId = externalProduct.Code;
product.Name = externalProduct.Name;
product.FullDescription = externalProduct.Summary;
product.ImageUrl = HybrisSettings.Url + externalProduct.ImageUrl; //custom property

//persist for next processor
args.Request.Properties["Product"] = product; //integration guide (page 12)
}
}

protected bool IsProductValidForImport(ExternalProduct product)
{
var result = true;
if (string.IsNullOrEmpty(product.Name)) // = item.Name
{
result = false;
Log.Info("Skip product ‚" + product.Code + "‘: Name not valid.", product); //NOTE System.Messages
}
return result;
}
}
}
[/csharp]

CustomProduct

Wenn auch die Bilder synchronisiert werden sollen, muss das Produkt-Template des Commerce Connect erweitert werden. Konkret müssen dafür folgende Anpassungen vorgenommen werden:

Template
Neues Template “CustomProduct” anlegen, welches vom Standard Commerce Connect Template „Product“ erbt.

Klassen
Erweiterte Produkt-Klasse „CustomProduct“ definieren:

[csharp]
using Sitecore.Commerce.Entities.Products;
using Sitecore.Data.Items;

namespace Comspace.Sitecore.CommerceConnect.Entities.Products
{
/// &lt;summary&gt;
/// Custom product including image field.
/// &lt;/summary&gt;
public class CustomProduct : Product
{
public string ImageUrl { get; set; }

public MediaItem Image { get; set; }
}
}
[/csharp]

Die Klasse „CustomProductRepository“ definiert dann wie das erweiterte Produkt gelesen und geschrieben wird:

[csharp]
using Comspace.Sitecore.CommerceConnect.Entities.Products;
using Comspace.Sitecore.CommerceConnect.Model.sitecore.templates.User_Defined.Comspace.CommerceConnect;
using Sitecore.Commerce.Data.Products;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Product = Sitecore.Commerce.Entities.Products.Product;

namespace Comspace.Sitecore.CommerceConnect.Data.Products
{
/// &lt;summary&gt;
/// Custom product repository including image field.
/// &lt;/summary&gt;
public class CustomProductRepository : ProductRepository
{
/// &lt;summary&gt;
/// Save product data to related item.
/// &lt;/summary&gt;
/// &lt;param name="entityItem"&gt;&lt;/param&gt;
/// &lt;param name="product"&gt;&lt;/param&gt;
protected override void UpdateEntityItem(Item entityItem, Product product)
{
base.UpdateEntityItem(entityItem, product);

using (new EditContext(entityItem))
{
var url = (product as CustomProduct).ImageUrl;

var image = GetMediaItem(url, product); //TBD
ImageField imagefield = entityItem.Fields[IProductConstants.ImageFieldName];
imagefield.Alt = image.Alt;
imagefield.MediaID = image.ID;
}
}

/// &lt;summary&gt;
/// Read product data from related item.
/// &lt;/summary&gt;
/// &lt;param name="entityItem"&gt;&lt;/param&gt;
/// &lt;param name="product"&gt;&lt;/param&gt;
protected override void PopulateEntity(Item entityItem, Product product)
{
base.PopulateEntity(entityItem, product);

ImageField imageField = entityItem.Fields[IProductConstants.ImageFieldName];
(product as CustomProduct).Image = imageField == null ? null : imageField.MediaItem;
}

#region Handle MediaItem
//…
#endregion
}
}
[/csharp]

.config
Angepasste Templates und Klassen in Sitecore.Commerce.Products.config registrieren:

[xml]
&lt;!– PRODUCT REPOSITORY –&gt;
&lt;productRepository type="Sitecore.Commerce.Data.Products.ProductRepository, Sitecore.Commerce"&gt;
&lt;patch:delete /&gt;
&lt;/productRepository&gt;
&lt;productRepository type="Comspace.Sitecore.CommerceConnect.Data.Products.CustomProductRepository, Comspace.Sitecore.CommerceConnect" singleInstance="true"&gt;
&lt;template&gt;{0C589D66-A119-435A-907F-43481CD5199F}&lt;/template&gt;
&lt;branch&gt;{0C589D66-A119-435A-907F-43481CD5199F}&lt;/branch&gt;
&lt;path ref="paths/products" /&gt;
&lt;Prefix&gt;Product_&lt;/Prefix&gt;
&lt;ProductsIndex&gt;commerce_products_master_index&lt;/ProductsIndex&gt;
&lt;ManufacturerRepository ref="productManufacturerRepository" /&gt;
&lt;DivisionRepository ref="productDivisionRepository" /&gt;
&lt;TypeRepository ref="productTypeRepository" /&gt;
&lt;ClassificationsRepository ref="productClassificationsFieldRepository" /&gt;
&lt;ResourcesRepository ref="productResourcesRepository" /&gt;
&lt;RelationsRepository ref="productRelationsRepository" /&gt;
&lt;GlobalSpecificationsRepository ref="productGlobalSpecificationsRepository" /&gt;
&lt;ClassificationsSpecificationsRepository ref="productClassificationsSpecificationsRepository" /&gt;
&lt;TypeSpecificationsRepository ref="productTypeSpecificationsRepository" /&gt;
&lt;/productRepository&gt;

&lt;includeTemplates&gt;
&lt;ProductTemplateId&gt;{0C589D66-A119-435A-907F-43481CD5199F}&lt;/ProductTemplateId&gt;
&lt;/includeTemplates&gt;

&lt;!– Commerce ENTITIES
Contains all the Commerce cart entities.
The configuration can be used to substitute the default entity implementation with extended one. –&gt;
&lt;commerce.Entities&gt;
&lt;Product type="Sitecore.Commerce.Entities.Products.Product, Sitecore.Commerce" &gt;
&lt;patch:delete /&gt;
&lt;/Product&gt;
&lt;Product type="Comspace.Sitecore.CommerceConnect.Entities.Products.CustomProduct, Comspace.Sitecore.CommerceConnect" /&gt;
&lt;/commerce.Entities&gt;
[/xml]

Stolpersteine

In der Klasse „Sitecore.Commerce.Templates“ existiert eine Konstante „ProductTemplateId“, deren Id fest verdrahtet ist und nicht auf die zuvor eingerichteten Konfigurationsdateien verweist. Die Konstante wird u.a. in der Klasse „ItemClassificationService“ verwendet, daher ist dieser auszutauschen:

[xml]
&lt;itemClassificationService type="Sitecore.Commerce.Products.ItemClassificationService, Sitecore.Commerce"&gt;
&lt;patch:delete /&gt;
&lt;/itemClassificationService&gt;
&lt;itemClassificationService type="Comspace.Sitecore.CommerceConnect.Entities.ItemClassificationService, Comspace.Sitecore.CommerceConnect" /&gt;
[/xml]

Resumé

Zuletzt möchte ich ein paar erste Eindrücke und Erkenntnisse zusammenfassen:

  • Der erste Einstieg durch Pipelines in Pipelines in Pipelines ist herausfordernd.
  • Es gibt umfangreiche Dokumentationen.
  • Einige hilfreiche Klassen aus Beispielen im Internet verweisen leider auf die CommerceServer API, z.B. ProductsSearchResult und CommerceConstants. Auch in dem Dynamics AX Demoshop von Sitecore (Commerce.Dynamics.Storefront) sind Verweise auf die CommerceServer API enthalten. Die Trennung bzw. Abstraktion ist m.E. noch nicht ganz sauber erfolgt.
  • Verwunderlich fand ich zunächst, dass die Synchronisation von Bildern nicht im Standard enthalten ist. Aber dem von Sitecore definierten Prinzip “kleinster gemeinsamer Nenner” folgend, ist es verständlich, denn Bild-Quelle und -Ziel sind doch sehr projektspezifisch.
  • Der “Merchandising Manager” ist NICHT Bestandteil des Commerce Connect, sondern des CommerceServer Connect.
  • In einem echten Projekt mit einem umfangreichen Datenmodell wird der größte Aufwand in das Verständnis und das Mapping der Datenmodelle gehen.

Wenn ihr mehr zum Sitecore Commerce Connect wissen wollt, dann schaut mal hier:

Sitecore Commerce Connect – Teil 1: Ein Überblick am Beispiel Hybris

Wie in meinem Vortrag zum Thema “Evaluierung einer Hybris-Anbindung an Sitecore” auf der letzten Sitecore Usergroup in Bielefeld versprochen, möchte ich mit dieser Beitragsserie unsere ersten Eindrücke von der Anbindung eines externen Shopsystems (in diesem Beispiel Hybris) an Sitecore mittels des Commerce Connect Moduls mit euch teilen.

In diesem Artikel geht es mir darum, euch einen ersten Überblick über das Marketing-Potenzial und Funktionsumfang des Moduls zu verschaffen. In den nächsten Beträgen wird es dann technischer mit konkreten Ausschnitten aus Konfigurationsdateien und Quellcode.

Warum sollten WebCMS und E-Commerce Platform verbunden werden?

Bevor wir an die technischen Details gehen, erlaube ich mir zunächst einige Gedanken aus Sicht von Produkt-Management, Marketing und Vertrieb auf Betreiberseite:

Webnutzer erwarten immer mehr eine einheitliche User-Experience, die sich über alle Kanäle und Plattformen eines Unternehmens erstrecken soll. Zudem  bringen auch Online-Shop Systeme kleinere CMS-Funktionalitäten mit und damit wird auch die Darstellung von Inhalten immer besser. Warum sollten dann CMS und Shop miteinander verbunden werden?

Sitecore bietet bereits in seiner eigenen Suite ein umfangreiches Bundle an Funktionen, um verschiedenste Touchpoints (Webseite, Newsletter, Social Media, Offline-Kanäle am POS usw.) zu einem Nutzererlebnis aus einem Guss zu verschmelzen.

Mit diesem Beispiel möchte ich Möglichkeiten aufzeigen, mit denen Besuchern auch auf Unternehmenswebseiten die Kernfunktionalitäten eines Online-Shops angeboten werden können (wie bspw. Produkte in den Warenkorb legen) und wie diese kombinierten Webseiten- und E-Commerce-Daten dann von der Unternehmenswebseite automatisch an das Shopsystem übergeben werden.

Ein Beispiel für eine Customer Journey mit Optimierungspotenzial sähe beispielsweise so aus:

Der Kunde schaut auf einer Webseite nach einem Produkt, muss sich das Produkt merken und im separaten Online-Shop danach suchen um zu bestellen. 

Das bedeutet:
Durch die zwei separaten Technologien Webseite und Shop entsteht für den Kunden Fehler- und Frustrations-Potenzial. Für Unternehmen und Shop-Anbieter kann das bedeuten: Erhöhte Abbruchgefahr und damit niedrigere Conversion-Rate.

Mit Lösungen wie dem Commerce Connect arbeitet die Technologie im Hintergrund für den Kunden, nimmt ihm Arbeitsschritte ab und erhöht damit die Wahrscheinlichkeit, dass es zu einem Verkauf kommt.

Der Endkunde bemerkt so den Technologiewechsel aus dem WebCMS in den Online-Shop überhaupt nicht und hat ein nahtloses Nutzererlebnis im Bestellprozess.

Kurzvorstellung Sitecore Commerce Connect

Sitecore Commerce Connect sieht sich als Framework und API zur nahtlosen Integration von 3rd-Party Commerce Systemen in Sitecore. Das Ziel ist die Überführung der Experience Marketing Features in die Commerce Welt:

  • Einheitliche Darstellung:  Produktkataloge, “normale” Inhalte und Bilder aus einem Guss
  • Bearbeitung von Produktkatalogen, Seiteninhalten und Bildern in einen System
  • Nutzung der Sitecore Facettensuche über ALLE Inhalte.
  • Personalisierung auch über das Kaufverhalten der Besucher
  • Tracking von Conversions und Values rund um den Einkauf
  • Identifizierung von und Einblick in die wertvollsten Kunden
  • Reaktion auf Änderungen im Geschäft, in Echtzeit

Konkreter Umfang des Commerce Connect Moduls

Das Sitecore Modul bietet die Möglichkeit Produktkataloge mit dem externen Commerce System zu synchronisieren, d.h. es stellt entsprechende Templates, Klassen, Repositories, Import-Pipelines etc. zur Verfügung:

Sitecore-Commerce-Connect-Modul

Erste Engagement-Pläne zu Warenkorb-Abbrüchen, neuen Aufträgen und Wiederverfügbarkeit von Produktbeständen werden mitgeliefert und genutzt:

Sitecore-Commerce-Connect-Engagement-Plan

Selbstverständlich kommen auch Erweiterungen für die Sitecore Rule Engine mit:

Sitecore-Commerce-Connect-Rule-Engine

Das Framework des Sitecore Commerce Connect 

Die Architektur wird von Sitecore wie folgt skizziert, wobei der Commerce Server Connector und der Dynamics AX Connector fertige Connectoren von Sitecore selbst sind und der Hybris Connector von uns im Rahmen der Evaluierung entwickelt wurde.

Service Layer:

Folgende Bereiche deckt das Modul ab:

  • Warenkorb
  • Preise
  • Kunden und Benutzer
  • Bestände
  • Produkte
  • Aufträge
  • Geschenkkarten
  • Wunschlisten
  • Bezahlung
  • Versand
  • Treueprogramme

Die einzelnen Layer sind optional und unabhängig voneinander. Ein Layer umfasst ein Datenmodell und Pipelines mit ggf. zugehörigen Service Providern. Die farblich markierten Service Layer wurden im Rahmen des PoC näher beleuchtet. Wenn ihr einen Eindruck haben möchtet was sich dahinter verbirgt, habt etwas Geduld – die zugehörigen Artikel folgen.

Prinzipien

Weil sie mir so gut gefallen haben, hier eine kurze Zusammenfassung und Übersetzung der Prinzipien welche dem Modul von Sitecore selbst auferlegt wurden:

  • Simplicity – kleinster gemeinsamer Nenner
  • Extensibility – Domain Model erweiterbar
  • Independently – keine Abhängigkeit zwischen Service Layern
  • Abstraction – keine Sitecore oder Shopsystem Abhängigkeit in Service Layern
  • Pipelines – jeder Service Layer benutzt Sitecore Pipelines
  • Fallback – fehlende Implementierungen im Shopsystem können überbrückt werden

Erstes Resumé

Mein Eindruck nach der Evaluierung des Commerce Connect anhand einer Hybris-Anbindung:

  • Der erste Einstieg ist herausfordernd, kurz stand ich davor wie “der Ochs vorm Berg”. Auf dem Partnertag in Frankfurt wurde von Sitecore eine Schulung zu dem Modul angekündigt. Die kann ich dann nur empfehlen.
  • Das bringt mich zu den zur Verfügung stehenden Materialien:
    • Es gibt umfangreiche Dokumentationen über das Modul im Developer Netzwerk.
    • Cookbooks gibt es leider keine, diese helfen sonst häufig die erste Hürde zu nehmen.
    • Wenn die Dokumentation nicht weiter hilft, gibt es im Internet aktuell leider noch wenig Material zum Commerce Connect.
    • Geholfen haben immer wieder die herunterladbaren Beispiel-Implementierungen für AX und CommerceServer und natürlich dotPeek.
  • Customizing gehört zum Grundkonzept. Durch eine gute Abstraktion ist fast alles flexibel anpassbar. Das gefällt, auch wenn es manchmal nicht einfach ist, im Pipeline-Dschungel den Überblick zu bewahren.
  • Für eine solide und benutzerfreundliche Integration ist eCommerce-KnowHow unumgänglich. Die dahinterliegenden Modelle und Prozesse müssen durchdrungen werden.
  • Die Komplexität einer Commerce Integration bringt auch höhere Konzeptions- und Abstimmungsaufwände mit sich als vielleicht sonst im CMS-Umfeld üblich.
  • Das Modul umfasst keine Layout-Komponenten und keine speziellen Applikationen.
  • Der “Merchandising Manager” ist  NICHT Bestandteil des Commerce Connect, sondern des CommerceServer Connect.
  • Eine gute Einsatzmöglichkeit für das Modul ist m.E. eine PIM-Integration.
  • Zu guter Letzt: Die Arbeit mit dem Modul hat mir Spaß gemacht!

Die Präsentation zum Vortrag:

In den kommenden Tagen werde ich diese Serie mit den folgenden Artikeln fortführen:

 

 

Sitecore Usergroup Deutschland – Entstehung, Ziele und Entwicklung

Workshop Kreativität

Was ist eine Usergroup?

Usergroups finden sich überwiegend im IT-Umfeld und verstehen sich als Anwendergruppen für eine bestimmte Software oder Technik. Die Mitglieder einer Usergroup sind bestrebt, die Software gemeinsam weiter zu entwickeln, ihre Interessen gegenüber der Herstellerfirma zu vertreten und einen regelmäßigen Erfahrungsaustausch zu pflegen.

Im Bereich der Content Management Systeme bestehen einige Usergroups bereits seit längerem, so z.B. die OpenText Usergroup und die OpenCMS Usergroup.

Von der Idee zum 1. SUG-Treffen

Sitecore Usergroup Deutschland LogoSitecore Usergroups wurden bereits bspw. in der Schweiz, den Niederlanden und den USA gegründet. Da das Sitecore CMS immer stärker auch bei Web-Projekten für den deutschen Markt eingesetzt wird, hat sich Christopher Wojciech von der netzkern AG zum Ziel gesetzt diese Community ebenfalls auch im deutschen Raum aufzubauen. Nachdem das erste Treffen in Köln ein voller Erfolg war, wollte man dies wiederholen. Aufgrund des Besuchs von webit! Gründer Sven Haubold in Köln hat man sich dazu entschlossen das nächste Treffen in Dresden zu veranstalten. Ein ebenso großes Engagement zeigte comspace aus Bielefeld und somit hat sich eine kleine Gruppe aus sehr motivierten Menschen gefunden, um die Sitecore Communtiy in Deutschland weiter nach vorne zu bringen.

Workshop Kreativität

 

Zielsetzung und Zielgruppen der Sitecore Usergroup

Da das Sitecore CMS in Deutschland einerseits noch nicht so lange auf dem Markt ist wie andere CMS-Lösungen, andererseits aber enormes Potential für digitale Marketingprojekte bietet und sich schnell weiter entwickelt, stehen der Erfahrungsaustausch und die gezielte Verbreitung von neuen Lösungen im Sitecore-Umfeld im Vordergrund.

Zu diesem Zweck wurden gemeinsam organisierte und regelmäßig stattfindende Veranstaltungen geplant, die Sitecore-Entwickler und -Anwender sowie Marketing- und IT-Verantwortliche zusammen bringen – sprich potentiell jeden, der in seinem Arbeitsalltag mit Sitecore zu tun hat, ob auf Agentur- oder Kundenseite. Diese Treffen dienen dem Wissensaustausch und der Weiterbildung, u.a. durch Fachvorträge, in denen über neue Lösungsansätze, Tools und Einsatzszenarios in der Praxis berichtet werden.

Auch außerhalb der Veranstaltungen soll ein fachlicher Austausch zu Erfahrungen, Ideen und auch Problemen mit dem Sitecore CMS nicht zu kurz kommen. Dazu dient z.Zt. eine Sitecore Usergroup Deutschland XING-Gruppe. Für die Zukunft ist eine Community geplant, die allen Sitecore Usergroup-Interessierten – unabhängig von einer XING-Mitgliedschaft – offen steht.

Nicht zuletzt bildet die Sitecore Usergroup eine gemeinsame Interessenvertretung von Anwendern und Partnern gegenüber Sitecore als Hersteller. Durch einen regelmäßigen Dialog mit Sitecore möchten wir die Wünsche und Anregungen der deutschen Anwender gesammelt und strukturiert kommunizieren.

Was die Usergroup nicht ist

Wichtig für die Organisatoren war es auch sich auf gewisse Rahmenbedingungen zu einigen. Dazu gehörte es festzulegen welche Zwecke die Sitecore Usergroup nicht verfolgt:

  •         Die Usergroup ist nicht der Platz für Kundengespräche.
  •         Die Usergroup dient nicht der Kundenakquise.
  •         Die Usergroup-Treffen sind keine Werbeveranstaltung.

Erfahrungen aus den ersten Sitecore Usergroup Treffen

Mittlerweile haben die ersten Sitecore Usergroup Treffen stattgefunden – im Herbst 2014 in Köln und im Frühjahr 2015 in Dresden. Die Events mit Vorträgen von Sitecore-Kollegen, – Partnern und -Kunden und mit einem kulturellen Rahmenprogramm wurden von allen Zielgruppen gut angenommen, auch wenn Entwickler von Sitecore-Partneragenturen in der Überzahl waren. Präsentiert wurden sowohl technische als auch Consulting-Themen oder Kundenprojekte, bspw. “Sitecore Health – Monitoring und Stabilität von Sitecore Umgebungen”, “Vernetzt ans Ziel – die neue Website der Dresdner Verkehrsbetriebe AG” oder Mehrwert für Webprojekte mit Sitecore und Microsoft Azure.

Wie geht es weiter?

Wir haben große und spannende Pläne um diese Community weiter zu vergrößern. Wir wollen eine kontinuierliche Plattform schaffen und dies auch in der Außenwirkung bekräftigen. Seid gespannt und kommt zum nächsten Treffen, dann erfahrt ihr mehr!

Daher lade ich Euch zu guter Letzt…

… herzlich zum 3. Sitecore Usergroup Treffen am 30. September 2015 im Historischen Museum in Bielefeld ein. Diese spannenden Themen stehen auf dem Programm:

  •         Evaluierung einer Hybris-Anbindung an Sitecore mittels Commerce Connect
  •         The good, the bad and the ugly Sitecore module
  •         Auf dem Weg zur Connected Customer Experience – Möglichkeiten zu omni-channel Strategien mit Sitecore

Ausführlichere Informationen zum Programm und die Möglichkeit zur Anmeldung finden Sie hier.

Learnings und Tipps aus dem Upgrade von Sitecore 7.5 auf Sitecore 8.0

Sitecore Update-Installation-Wizard

Unser erstes Projekt-Upgrade von Sitecore 7.5 auf Sitecore 8.0 ist abgeschlossen und wir wollen es nicht versäumen, unsere Erfahrungen aus diesem Prozess mit Ihnen zu teilen. Als Ausgangsbasis für den Upgrade Prozess dient wie bei allen Sitecore Upgrades der „Sitecore Upgrade Guide“.

Dieser Guide bietet eine gut nachzuvollziehende Schritt-für-Schritt-Anleitung

Im wesentlichen besteht dieser Prozess aus den folgenden Schritten:

  1. Backup von Website und Datenbanken
  2. Ausführung von SQL Server Update Skripten auf den Sitecore Datenbanken
  3. Installation des Upgrade Packages über den “Update Installation Wizard”
  4. Aktualisierung der Konfigurationsdateien
  5. Neuaufbau der Suchindizes und der Link Datenbank
  6. Ggf. Aktualisierung weiterer Module (z.B. “Web Forms For Marketers”-Modul)

Sitecore Update-Installation-Wizard
Sitecore Update-Installation-Wizard

Nach dem Upgrade-Prozess sahen wir uns mit einigen Problemen konfrontiert. Dazu gehörten sowohl Issues im Sitecore Client als auch die Aggregation der Analytics-Daten in die Reporting Datenbank.

Probleme und Lösungen beim Laden des Sitecore Clients

1. Fehlermeldung „Could not load type ‚Sitecore.Shell.Applications.WebEdit.Commands.WebEditCommand‘ from assembly ‚Sitecore.Client, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null‘.“

Hintergrund ist die Einführung der neuen “Sitecore.ExperienceEditor”-Assembly. Wir verwenden eine Reihe von Custom Buttons im Page Editor. Diese hingen vorher vom “Sitecore.Shell.Applications.WebEdit.Commands”-Namespace ab. Dessen Funktionen sind mit Sitecore 8 nun in die neue Assembly “Sitecore.ExperienceEditor.dll” ausgelagert worden.Die Lösung bestand darin, in der Visual Studio Solution einen Verweis zu dieser neuen Assembly hinzuzufügen und das Projekt neu zu bauen.

2. Fehlermeldung „Could not resolve type name: Sitecore.ContentSearch.Events.PublishingEventHandler, Sitecore.ContentSearch (method: Sitecore.Configuration.Factory.CreateType(XmlNode configNode, String[] parameters, Boolean assert)).“

Durch den Austausch der Sitecore.ContentSearch gegen die Default Sitecore 8 „Sitecore.ContentSearch.config“ bzw. „Sitecore.ContentSearch.dll“ konnte dieses Problem schnell ausgeräumt werden.

3. jQuery Konflikt mit Prototype im Sitecore 8 Experience Editor

JavaScript-Konflikte traten durch den parallelen Einsatz der JS-Frameworks Prototype und jQuery zu Tage. Mittels Wrapper-Workaround konnte dieses Problem umgangen werden.

Probleme und Lösungen bei der Aggregation der Analytics-Daten in die Reporting-Datenbank

1. Leere „Segments“- und „TreeDefinition“-Tabellen in der Reporting-Datenbank.

Als Workaround wurden die betreffenden Tabellen mit einer “sauberen” Default Sitecore 8.0 Reporting Datenbank manuell abgeglichen und ergänzt.

2. Fehlermeldung „Exception: System.IO.FileFormatException
An error occurred while deserializing the Pages property of class Sitecore.Analytics.Model.VisitData: An error occurred while deserializing the PageEvents property of class Sitecore.Analytics.Model.PageData: An error occurred while deserializing the CustomValues property of class Sitecore.Analytics.Model.Entity: Die“ Zeichenfolge kann keine Länge von 0 (nu ll) haben.““

Diese Exceptions sind der Tatsache geschuldet, dass das WFFM 2.5 Modul in Sitecore 7.5  Ereignisdaten in der Analytics MongoDB in anderer Weise abbildet und speichert als mit WFFM 8.0. Zudem werden unterschiedliche Klassen zur Deserialisierung während der Aggregation verwendet.Eine Lösung konnte dank des Sitecore-Supports umgesetzt werden: Dieser lieferte entsprechende Patches zur Aggregation historischer WFFM-Analytics-Daten.

Weitere Tipps für eine reibungslose Aktualisierung von Sitecore 7.5 auf 8.0

Folgende Punkte halten wir generell aus unseren Erfahrungen mit dem Upgrade auf Sitecore 8.0 für erwähnenswert:

1. Besondere Aufmerksamkeit sollte auf die Aggregation der Analytics-Daten und zusätzlicher Module wie WFFM oder EXM gelegt werden. Falls diese Anwendungen gar nicht erst eingesetzt werden sollen, sollte das Upgrade kaum Probleme bereiten.

2. Custom Buttons im Experience Editor können übergangsweise zunächst auf die alte SheerUI umgestellt und dann ggf. in das neue SPEAK-Framework umprogrammiert werden. Hilfreiche Hinweise zu diesem Thema liefert der Artikel “A New Look to Buttons in Experience Editor”.

3. Eine Übersicht über bereits bekannte Bugs in Sitecore 8 liefert die Sitecore Knowledge Base-Seite.

Welche Erfahrungen haben Sie mit einem Upgrade auf Sitecore 8.0 gemacht und welche Tipps können Sie anderen Sitecore-Entwicklern geben?

Wir freuen uns auf Ihre Kommentare.

Baut uns doch mal eine Referenz City

Unser Kunde Xella Deutschland stellt massive Bausteine her, aus denen Häuser aller Art und Größe gebaut werden. (Hier haben wir schon einmal darüber berichtet, wie es in einem Xella-Werk zugeht.) Was liegt für ein solches Unternehmen also näher, als über 25 Referenz-Gebäude seiner Marken Ytong, Silka und Multipor zu einer Referenz City zusammen zu stellen, die sich aus den Referenz-Objekten im OpenText CMS generiert.

Xella-Multipor-Referenz-City

Damit war die Idee zur Referenz City geboren.

Zunächst einmal war eine sorgfältige Anforderungsanalyse notwendig. Bei Elementen, die nicht zum Standard-Baukasten einer Website gehören, ist das besonders wichtig. Wir mussten genau definieren, was bei welchem Klick passieren sollte und wie das alles aussehen musste. Ohne eine solche Analyse kann es schnell passieren, dass der Kunde bildlich gesprochen vom einem Pudel spricht, aber einen Dackel geliefert bekommt.

Basierend auf der Anforderungsanalyse konnte ich dann die Umsetzung konzipieren und abschätzen, wie lange ich dafür brauchen würde oder wo wir noch genauere Informationen benötigten.

WAGA, unser Kreativpartner in diesem Projekt, hat verschiedene Möglichkeiten der Darstellung konzipiert und wir haben deren Umsetzung evaluiert.

Technologische Herausforderungen einer Panorama-Anwendung

Die Schwierigkeit lag darin, dass es sich hier nicht um einen “normalen” Slider handelte, sondern eine individuell steuerbare Panorama-Anwendung. Für diesen speziellen Fall konnte ich daher kein passendes Plugin finden, dass genau den Anforderungen des Kunden entsprach. In Folge dessen musste ich selbst etwas programmieren, das in allen modernen Browsern funktioniert und nebenbei auch noch responsives Verhalten an den Tag legt.

Die “Referenz City” habe ich dann mit Hilfe von HTML, CSS und JavaScript (jQuery) umgesetzt. Die Nutzung der JavaScript-Bibiothek jQuery hat mir an manchen Stellen zwar geholfen, aber die Logik für die Benutzung konnte sie leider nicht liefern. Somit habe ich nahezu die komplette Panorama-Anwendung selbst programmiert.

Die Features der Referenz City im OpenText CMS

Momentan setzt sich die “Referenz City” aus 4 verschiedenen Bereichen zusammen: Es gibt den Einleitungs-/Intro-Bereich, die Referenzen der Marke Ytong, die Referenzen der Marke Silka und die Referenzen der Marke Multipor.

  1. Unter der City befinden sich die verschiedenen Markennamen. Klickt man auf einen der Namen, fährt die Ansicht der City direkt zu dem Bereich in dem sich Häuser (Referenzen) dieser Marke befinden. Die Markennamen, zu denen dann Referenzen in der City sichtbar werden, werden dann farblich hervorgehoben (in diesem Fall grau).
  2. Neben den Markennamen sieht der Nutzer auf größeren Geräten auch noch 2 Pfeil-Icons. Mit Hilfe der Pfeile kann der Besucher ebenfalls durch die City navigieren. Auf mobilen Geräten (z.B. iPhone) sind die Pfeile nicht sichtbar. Stattdessen kann der Nutzer hier über die Stadt wischen. Je nach Wischrichtung bewegt sich die Stadt entweder nach links oder rechts.
  3. Desweiteren kann der Besucher der Website auch per Druck auf die Pfeiltasten seiner Tastatur (sofern vorhanden) die City nach links und rechts bewegen.
  4. Sobald man dann ein Haus in der City gefunden hat, über das man mehr erfahren möchte, kann man mit dem Maus-Cursor übers Gebäude fahren und es erscheint ein gelber Kreis, in dem sich der Titel dieser Referenz befindet.
  5. Klickt man dann auf den Kreis oder das Haus, öffnet sich ein Pop-Up auf der Seite. Dieses Pop-Up beinhaltet ein Foto der Referenz, einen Titel, einen kurzen Text über die Referenz und einen Button, über den man auf eine andere Seite weitergeleitet wird. Diese Seite enthält dann Detailinformationen zu der Referenz.
  6. Öffnet man die “Referenz City” auf mobilen Geräten (z.B. iPhone) so kann man nicht mit einem Maus-Cursor über die Häuser fahren. Stattdessen erscheinen über allen Referenzen kleine gelbe Kreise mit Pfeil nach unten, die sich über dem Gebäude auf und ab bewegen. Sie zeigen dem Besucher der Seite so, dass dieser auf die Gebäude klicken/touchen kann um so mit der City zu interagieren.
  7. Die Integration ins OpenText hat keine besondere Schwierigkeit dargestellt. Eigentlich hätten wir das auch mit jedem beliebigen anderen CMS realisieren können. Wir mussten nur pro Referenz die Pflege der Bilder und Texte im CMS ermöglichen.
  8. Aus der Liste der Referenzen wird die “Referenz City” generiert. Kommt eine neue Referenz hinzu, muss lediglich die entsprechende Grafik im Panorama angepasst werden.

 Welche Einsatzszenarien wären noch möglich?

Denkbare Alternativen wären

  • Präsentation einer Produktpalette
  • Darstellung einer Wertschöpfungskette
  • Präsentation einer Customer Journey
  • Firmenhistorie als Zeitstrahl
  • usw.

Generell lassen sich Daten aus Content Management Systemen heraus im Web auf viele verschiedene Arten darstellen. Ob als Diagramm, Tabelle, Weltkarte oder eben ein interaktives Panorama. Dabei ist die Darstellung der Informationen prinzipiell unabhängig vom CMS. Zu bedenken sind aber technische Grenzen wie die Browserkompatibilität oder auch das Verhältnis von Aufwand und Nutzen.

Hier kommen Sie direkt zur Referenz-City von Multipor.