November Happy Hour will be moved to Thursday December 5th.

Magnus Rahl
Jul 25, 2018
  4878
(4 votes)

Multi-site support in the Personalization Native Integration for Commerce

This week we released version 2.0 of the Commerce native integration package for Personalization (Perform/Reach), EPiServer.Personalization.Commerce.

The reason for the new major version is that we have had to make breaking changes in some public APIs to enable the new long awaited feature in this release: Multi-site support. For current implementations the required changes are very limited.

Introduction to the scope concept

Central to the multi-site support is the concept of a scope. Tracking requests in the Personalization native integration APIs as well as the catalog feed export are now, explicitly or implicitly, associated with a scope. Each scope has its own Personalization service account configuration. Tracking, recommendations and catalog feeds are therefore completely siloed for each site even if the sites run the same code in an Episerver multi-site solution.

We have strived to build the multi-site support around the scope concept in such a way that it is open and extensible but with default behavior that will allow many solutions to use it without, or with only minimal, customization.

I will try to illustrate the different new capabilities by using a number of scenarios. These are scenarios are based on actual customer cases that we have used in framing and designing the new feature. The scenarios are somewhat ordered by increasing complexity, but the list is neither complete in the sense of listing every imaginable scenario nor in using every possible customization.

Scenario 1: Single site solution

This scenario hasn't really changed since the previous version. You can keep the same API calls for tracking and recommendations. The configuration does not need to define any scopes, you can keep the old configuration keys, e.g.

<add key="episerver:personalization.BaseApiUrl" value="myhost.peerius.com"/>
<add key="episerver:personalization.Site" value="mysite"/>
<add key="episerver:personalization.ClientToken" value="myClientToken"/>
<add key="episerver:personalization.AdminToken" value="myAdminToken"/>

Note however that the old configuration keys from the beta stage are no longer supported, only the keys with "episerver:personalization." prefix.

If you have used the extension points in namespace EPiServer.Personalization.Commerce.CatalogFeed to customize the catalog feed export you will notice that the interface definitions have changed and you need to add a string scope parameter to your implementations. You will however not need to use that new parameter for anything in this scenario.

Scenario 2: Multi-site solution with separate catalogs and SiteDefinition scope

In this scenario, each scope corresponts to a site definition (site defined in "Manage Websites" in Episerver CMS Admin mode), i.e. there is a one-to-one mapping between a site definition and a Personalization service account. Furthermore, each site displays products from its own distinct catalog.

The site definition scope is what we consider the default, and this is reflected in the tracking and recommendation API (CommerceTrackingAttribute, ITrackingService.Track extension methods) in the way that you do not need to specify a scope. It will default to the site definition of the current request.

You do however need to provide configuration for the different Personalization service accounts, mapped to the correct scope. Here's an (incomplete) example configuration:

<add key="episerver:personalization.ScopeAliasMapping.Alias1" value="eec884ae-4272-4710-99d0-392cce987422"/>
<add key="episerver:personalization.ScopeAliasMapping.Alias2" value="8ac1c69d-e03f-4831-8c79-07c850ca67ec"/>
<add key="episerver:personalization.BaseApiUrl.Alias1" value="host1.peerius.com"/>
<add key="episerver:personalization.BaseApiUrl.Alias2" value="host2.peerius.com"/>
<add key="episerver:personalization.CatalogNameForFeed.Alias1" value="Site1Catalog"/>
<add key="episerver:personalization.CatalogNameForFeed.Alias2" value="Site2Catalog"/>

As mentioned the default scope corresponds to the site definition, respresented by its Guid ID (from EPiServer.CMS.UI 11.5.0 this is visible in admin mode, but it can be found in earlier versions as well). To make the rest of the configuration simpler, you are first required to define an alias for each scope. The catalog feed export job will also iterate over these defined scopes to create the catalog feed for each Personalization service endpoint.

Each scope alias is then used to define the required endpoint settings (BaseApiUrl, but also Site, ClientToken and AdminToken omitted in this example for brevity) and the name of the catalog to export for this site.

Scenario 3: Multi-site solution with shared catalog and SiteDefinition scope

The configuration in this scenario is the same as Scenario 2, except it will define the same catalog name for both scopes (or omit this setting, in which case the first catalog is used for export).

For this to work, the products in the catalog need to have distinct URL:s in the exported catalog feed, and the URL:s should be the display URL for the product in each site where it exists. This can come in several sub-scenarios (again, this is not an exhaustive list):

Scenario 3A: Product URL:s differ only in site domain

In this scenario, all products in the catalog are available in all sites, and their relative URL:s are the same in all sites, e.g. www.site1.com/products/productX and www.site2.com/products/productX. This scenario normally does not require any additional steps, as the catalog feed export by default will try to resolve the scope as a site definition ID and then use that site definition's URL to convert the resolved relative URL to an absolute URL.

Scenario 3B: Product URL:s relative part differs in different sites.

In this scenario, the relative URL is constructed in different ways for different sites. By default the relative URL is built by the default URL resolver, which uses the primary parent category of the product to build the URL. This can be customized by registering a custom implementation of the IEntryUrlService interface (namespace EPiServer.Personalization.Commerce.CatalogFeed):

public class CustomEntryUrlService : IEntryUrlService
{
    public string GetExternalUrl(EntryContentBase entry, string scope)
    {
        // Construct the relative URL of the provided entry when viewed in
        // the provided scope.
    }
}

Scenario 3C: Distinct set of products displayed in each site

In this scenario, only a subset of the catalog, e.g. a specific root category or products with a certain "tag" are displayed in each site. To support this you need register a custom implementation of the ICatalogItemFilter interface (namespace EPiServer.Personalization.Commerce.CatalogFeed):

public class CustomCatalogItemFilter : ICatalogItemFilter
{
    public bool ShouldFilter(CatalogContentBase content, string scope)
    {
        // Determine if the provided content is to be included in the provided scope,
        // e.g. by looking at its categories or a custom property.
    }
}

Depending on your URL structure you may also need to couple this with the approaches from 3A or 3B.

Scenario 4: Multi-site solution with products shared between catalogs

I list this scenario separately to highlight an important point. It might look like Scenario 2 since there are multiple catalogs, the requirements are more like Scenario 3. Products are cross-linked between catalogs, for example by using a central resource catalog and then linking products to the site specific catalogs. This means that you need to make sure that the URL:s are correct when exporting the catalog feed for each site. Depending on your (primary) category structure you could otherwise end up with the wrong URL:s in one, several or even all your catalog feeds, and probably also duplicated URL:s across feeds, which is not allowed.

Scenario 5: Single or multi-site solution with custom sub-site scope definition

In this scenario, the scope does not map one-to-one with a site definition, but instead the current scope is set by the implementation. For example it could be a subsection of the site. This requires the implementation to pass the scope name to the tracking method calls:

var scope = "customScope1";
var trackingData = _trackingDataFactory.CreateProductTrackingData(productCode, httpContext);
var responseData = await _trackingService.TrackAsync(trackingData, httpContext, currentContent, scope);

The configuration would again be similar to Scenario 2, but define settings for the custom scopes instead of (or in addition to) the default site definition scopes:

<add key="episerver:personalization.ScopeAliasMapping.Alias1" value="customScope1"/>
<add key="episerver:personalization.ScopeAliasMapping.Alias2" value="customScope2"/>

In this scenario have to register a custom implementation of the IFeedUrlConverter interface (namespace EPiServer.Personalization.Commerce.CatalogFeed), which is used to convert relative URLs in a scope to absolute URLs. As mentioned in Scenario 3A the default converter will try to interpret the scope as a site ID and use the site URL, so with a custom scope definition you will need to do this conversion yourself:

public class CustomFeedUrlConverter : IFeedUrlConverter
{
    public string GetExternalUrl(string relativePath, string scope)
    {
        // Determine the base URL for the scope and use it to convert
        // the provided relative path to an absolute URL.
    }
}

Note that the IFeedUrlConverter implementation will be used both when making product URL:s absolute for the catalog feed data, but also to create the URL for the feed download endpoint (used in the callback from the Personalization service). Normally this conversion should be the same, but it is worth pointing out that the feed download URL should also be rewritten to one that is accessible from the internet so that the Personalization service can reach it.

You also have to register a custom implementation of the ICookieService interface (namespace EPiServer.Personalization.Common) and store the session information for the different scopes in different cookies, as you will be keeping several Personalization sessions alive on the same domain.

Depending on your catalog setup you may also need to implement IEntryUrlService as in scenario 3B and/or ICatalogItemFilter as in scenario 3C to make sure each scope has contains a distinct set of products with the correct URL:s used to view them.

Scenario 6: Multi-site solution with custom cross-site scope

In this scenario, the same scope is used on several sites. It could be one global scope, or it could be multiple cross-cutting sub-site scopes, i.e. products under brand1.company.com/accessories and brand2.company.com/accessories are considered in the same scope, while brand1.company.com/spareparts is in a different scope.

This scenario basically has the same requirements as Scenario 4. An addition is that since the Personalization session information is stored in cookies and the cookies are set per domain, the visiting users will by default get separate sessions and identities for each of the domains, even if they are in the same scope. If your sites run on subdomains of the same main domain you can however use your ICookieService implementation to set the domain of the cookies to the main domain and through that get Personalization sessions that flow across the domains.

Final words

Other scenarios than the ones listed above are certainly possible, but I hope these serve to illustrate the most common ones and some advanced ones, as well as introducing some of the new and updated extension points that allow customization for multi-site solutions.

For more information, consult the breaking changes article for Personalization 2.0 and the developer guide article about the personalization multi site feature.

Jul 25, 2018

Comments

Please login to comment.
Latest blogs
Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog

ExcludeDeleted(): Prevent Trashed Content from Appearing in Search Results

Introduction In Optimizely CMS, content that is moved to the trash can still appear in search results if it’s not explicitly excluded using the...

Ashish Rasal | Nov 7, 2024