SmartEdit: A Deep Dive Into Technical Aspects Of the New CMS & Personalization Capabilities
Non-product content management has for many years been the weakest link of the SAP Commerce platform. Formally, the suite had a WCMS module from the very first version, but it was obvious to everybody that the solution was terribly old-fashioned and outdated.
Already back in 2016, SAP Hybris Commerce was extended with a new solution, SmartEdit. On the one hand, SAP released it too early. Many commented that the product is underbaked to replace WCMS Cockpit. Even two years later, Smartedit had drawn much criticism from the users and developers. On the other hand, it was long overdue. We had been waiting for a replacement for years.
In the search of the truth, in 2018, I was examining Smartedit in detail and shared the findings here on Hybrismart. There hasn’t been a lot of water under the bridge since 2018, but the situation with Smartedit has taken a definite turn for the better. However, being a “non-mandatory” component during two years, it was generally ignored by the community. After all, why should we use Smartedit if there is a good old WCMS Cockpit, time-tested and proven, albeit with known issues and poor customizability? This is why only a small percentage of developers are aware of Smartedit from the technical perspective. I hope this article will help them to move forward.
It should be noted that starting from the version 1811, we’re currently out of options. The cockpit framework, and with it the WCMS Cockpit and BTG personalization, were completely removed from the platform. So, Smartedit is the only option for content management and personalization.
This layer can be presented in different functional modes, from the basic one to advanced. Each mode provides a set of available content operations and can be restricted to a content management role. There are five modes out of the box: Preview, Basic Edit, Advanced Edit, Versioning, and Personalization.
The content delivery layer is completely decoupled from the content management. Smartedit is responsible for the content management part only. Content delivery and rendering are based on the old good Spring MVC.
For the pages, components, and templates, the data model is generally the same we had with WCMS.
Smartedit is designed to work with any storefront supporting the well-documented Smartedit storefront integration contract. Currently, only two storefronts support the contract, the built-in JSP-based SAP Commerce Accelerator, and a brand-new javascript storefront, Spartacus.
The modular structure helps to extend almost everything. The extensibility capabilities are demonstrated with a personalization module, which can also be used as a reference implementation for your own custom modules.
Example code:
The attributes and type details are exposed to Smartedut via Structure API. By default it provides converters for Collections, Maps, Primitive Wrapper classes, Dates, Products, Categories, User Groups, Media Containers, CMS Items, and all localized variations of these types. For blacklisting the specific attributes, list these in a defaultCmsStructureTypeBlacklistAttributeMap.
If all slots have ValidComponents configured with the specific set of allowed components, if your new component type is outside this list (merged across the slots), it won’t be displayed in Smartedit.
This code creates “A NEW BUTTON” in the Perspective Toolbar:
With this service, you can customize all three toolbars:
The name of the toolbar, Header, Experience or Perspective, is specified in the addToolbarItem.toolbarId.
If you decide to add a custom Wizard, add a callback function implementation into a toolbar definition (see above):
and your custom service (myService.ts):
Content Management Systems
To better understand the concepts of Smartedit and its place in the market, let’s look back at how content management systems have evolved over time. The first CMSs were developed to support only websites. Mobile, desktop, and kiosk versions were considered as separate channels. As a result, many of the solutions were designed as monolithic applications in which the user interface and data access code are combined into a single program to form a single platform. When it comes to innovation, most of the CMS solutions are constrained by their legacy architecture. This was one of the reasons why CMS Cockpit had not been evolved for years by SAP. With every single year of dealing with the deprecating software, the developer experience has been a key pain point which boils down to lack of vendor support, poor documentation, problematic troubleshooting and debugging. The traditional monolithic CMSes provide both content management and content rendering. More specifically, they have been developed with the frontend and backend being designed into a single platform. In Hybris, we had Accelerator templates built in JSP, page controllers built in Java, and all this stuff must be aligned with the data model and architecture of the CMS system. The WCMS was designed mainly for developers thereby reducing the leverage of these systems by non-technical users. Many concepts require an understanding of the underlining data model. Headless CMS has no default front-end system to determine how the content is presented to the end user. It provides all of the capabilities of the backend of a traditional CMS (i.e., the “body”) while giving the responsibility of content presentation to the delivery channels (i.e., the head). It acts as a content repository. The data can be requested or pulled from the CMS by any channel, such as mobile or kiosk app, by way of a RESTful API. Each individual channel takes advantage of their own unique presentation capabilities. Some would also name those content management systems “API driven CMS”. The problem with most of headless CMS is you need one more layer to be managed somehow, presentation layer. The possibility to have a page-oriented approach with layouts or grids is important, but generally not provided by headless CMS solutions.Smartedit
Smartedit was designed to bring the best of both worlds. Its architecture combines the flexibility and adaptability of API-first approach and the user-friendliness of a traditional CMS. For the stable set of API interfaces, you can create more than one storefront and even more than one content management client. For example, on top of the CMS API, you can develop a module that regularly checks the content and performs corrective actions if it doesn’t meet requirements. Smartedit is built as a single page app interacting with the CMS API. Like a storefront, it makes Smartedit headless too, so that theoretically you can have an alternative client app for CMS operations. Such architecture makes Smartedit clear and understandable in terms of architecture, customizability and data flows. However, Smartedit introduces a bunch of new technologies and tools. It imposes stricter requirements to the project team. It is very unlikely you will use old and obsolete AngularJS 1.6 for the storefront too. The reason for that is clear. Being a single page app, the essential part of Smartedit belongs to frontend development. Frontend world is crazy: it has changed fundamentally in the last 4-5 years. Multiple tools and libraries rose up to meet the challenge, and the best ones have slowly floated to the top. Years ago AngularJS was a promising toolset. As we can see from the code, roadmap and Smartedit releases and updates, the product has been making great headway in refactoring and redesign. The extendable components are now in Typescript, and the interfaces have been evolving to meet the market standards.Key concepts of Smartedit
Smartedit maintains content in a “page-oriented” manner. Content is generally stuck to the components it is used in. You can’t browse a list of all banners in the system to manage their parameters in isolation from the presentation configuration. Instead, you need to open a page, and select a banner on the page, and change the configuration for the particular banner instance. All alternative approaches will require deep customization of the system. The page editor is integrated into the website UI. It makes Smartedit look like a transparent layer on top of the storefront.Without Smartedit | With Smartedit |
---|---|
Key components
There are three functional components, Navigation Management, and Page Management and Personalization. In terms of navigation, there is a fourth component, Versioning, but functionally it is part of page management. As a name of the CMS feature, “Versioning” was really confusing to me. It is not an Undo/Redo functionality as I’d have thought. Functionally, Smartedit’s versioning is closer to Backup/Restore: you can save a current version before making changes, and revert to the saved version to discard the changes and restore the saved state. Currently, versioning in SmartEdit is only by page. Content slot and component versioning are not available yet but promised to be added in the future. Navigation management is based on similar concepts as found in good old CMS Cockpit. You can add, move, edit and delete navigation nodes, change the order of the navigation nodes using drag and drop feature. Smartedit’s UI is faster, sleeker, and generally more convenient than what we had in WCMS Cockpit. However, a few hiccups are still a struggle for newbies. For example, you can’t publish the changes in navigation directly from the page where these changes were done. You need to synchronize a navigation component which is in a completely different corner. The navigation data model makes it difficult for many people who are not familiar with complex data structure handling and not experts in this matter at all. How to explain the different types of navigation entry to non-specialist? However, these cases are few. The Smartedit’s personalization module replaced the old similar-purpose component, Behaviour Target Groups, or Advanced Personalization. The legacy solution triggers actions or shows CMS content to the different group of customers based on their behavior, interests, and historical data. You could use customer-specific promotions, suggest the products, or adapt the content and products of the website to their interests. It looks great at the demonstrations, but due to significant impact on performance, this module was generally avoided. The new module, personalization module, is promising.Architecture of SmartEdit Personalization
Personalization module is based on two pillars:- User/Segment assignments. The segment is used to group the users having the similar behavior or attributes.
- For example, all users come from the e-mail ad can be grouped into the “Email ad” segment.
- For example, all users with the age from 40 to 50 can be grouped into the “40-50 y.o.” segment.
- Page or component actions, variations, and the triggers to activate a group of variations.
- For example, for users assigned to the combination of the segments “Email ad” and “40-50 y.o”, the banner on the main page is different.
- RECALCULATE internally calculates the variations and caches the results in the session for the further calls,
- code: calculateAndLoadInSession(user)
- UPDATE pulls the user/segment assignment updates from the external system,
- code: updateSegments(user, configData.getUpdateProviders())
- ASYNC_RECALCULATE initiates the asynchronous variation recalculation process.
- asyncRecalculate(user, configData.getAsyncProcessProviders())
- LOAD loads the personalization results from the database and saves them serialized as a session var
- loadResult(user)
Customizing Smartedit
There are two main techniques (used together) for customizing the business logic and interfaces:- Extending APIs
- Configuring existing
- Rewriting existing
- Creating new APIs
- Extending frontend business logic and presentation
- Menu items, popups, buttons…
Global permissions | Catalog permissions |
https://localhost:9002/ permissionswebservices/ v1/ permissions/ principals/ admin/ global?permissionNames= smartedit.configurationcenter.read | https://localhost:9002/ permissionswebservices/ v1/ permissions/ principals/ admin/ catalogs?catalogId= electronicsContentCatalog &catalogVersion=Staged |
Response: | Response: |
/* Copyright (c) 2017 SAP SE or an SAP affiliate company. All rights reserved. */
angular.module('personalizationsmarteditRulesAndPermissionsRegistrationModule', [
'permissionServiceModule',
'personalizationsmarteditServicesModule'
]).run(function($q, permissionService, personalizationsmarteditRestService, personalizationsmarteditContextService) {
var getCustomizationFilter = function() {
return {
currentPage: 0,
currentSize: 1
};
};
// Rules
permissionService.registerRule({
names: ['se.access.personalization'],
verify: function() {
return personalizationsmarteditContextService.refreshExperienceData().then(function() {
return personalizationsmarteditRestService.getCustomizations(getCustomizationFilter()).then(function() {
return $q.when(true);
}, function(errorResp) {
if (errorResp.status === 403) {
//Forbidden status on GET /customizations - user doesn't have permission to personalization perspective
return $q.when(false);
} else {
//other errors will be handled with personalization perspective turned on
return $q.when(true);
}
});
});
}
});
// Permissions
permissionService.registerPermission({
aliases: ['se.personalization.open'],
rules: ['se.read.page', 'se.access.personalization']
});
});
angular.module('personalizationsmarteditRulesAndPermissionsRegistrationModule', [
'permissionServiceModule',
'personalizationsmarteditServicesModule'
]).run(function($q, permissionService, personalizationsmarteditRestService, personalizationsmarteditContextService) {
var getCustomizationFilter = function() {
return {
currentPage: 0,
currentSize: 1
};
};
// Rules
permissionService.registerRule({
names: ['se.access.personalization'],
verify: function() {
return personalizationsmarteditContextService.refreshExperienceData().then(function() {
return personalizationsmarteditRestService.getCustomizations(getCustomizationFilter()).then(function() {
return $q.when(true);
}, function(errorResp) {
if (errorResp.status === 403) {
//Forbidden status on GET /customizations - user doesn't have permission to personalization perspective
return $q.when(false);
} else {
//other errors will be handled with personalization perspective turned on
return $q.when(true);
}
});
});
}
});
// Permissions
permissionService.registerPermission({
aliases: ['se.personalization.open'],
rules: ['se.read.page', 'se.access.personalization']
});
});
- SmartEdit side:
- Define a rule “se.access.personalization” => REST CALL to /customizations. If 403, rule result is NO ACCESS
- Define a rule “se.read.page” => catalogVersionPermissionService.hasReadPermissionOnCurrent()
- Define a permission
- “se.personalization.open” = ”se.read.page” and “se.access.personalization”
- perspectiveService creates a personalization perspective with a constraint: “se.personalization.open”
- Combined view toolbar item is created with a constraint:
- “se.read.page”
- SAP Commerce side:
- /customization access is driven by spring security (user roles) and oauth scopes
- CMS component types (code, name, exposed attributes)
- CMS component type attributes (qualifier, type, localization info)
- httpAuthInterceptor – adds an authentication token to all REST requests
- httpErrorInterceptor – handles all 401 (unauthorized access) or OCC validation errors
- i18nInterceptor – appends a locale to an URL for the request and postprocess the response
- experienceInterceptor — adds a current catalog and version to the /cmswebservices/catalogs/
Smartedit contract for the storefront
The SmartEdit framework is capable to work on any storefront that implements the SmartEdit Contract. The SmartEdit Contract consists of the following:- webApplicationInjector.js JavaScript file, to be included in each page that you want to edit with SmartEdit
<script src="some/location/webApplicationInjector.js" data-smartedit-allow-origin="domain1, domain2"></script>
- An HTML markup contract for content slots and components
<div class="smartEditComponent" data-smartedit-component- type="SimpleResponsiveBannerComponent" data-smartedit-component-id= "SAPCommerceComponentName" data-smartedit-component-uuid="somecompositekeyserialization" data-smartedit-catalog-version-uuid="apparel-deContentCatalog/Staged"> <your-original-component/> </div>Body:
<body class="smartedit-page-uid-mypageuid smartedit-page-uuid-mypageuuid smartedit-catalog-version-uuid-mycatalogversionuuid">
- A preview ticket API mechanism
- Optional: reprocessPage and renderComponent functions that perform frontend rendering after the addition or modification of content
Modules
The following diagram shows how Smartedit modules interact with the platform:- previewwebservices — Preview API. Preview ticket operations
- cmswebservices – CMS API for retrieving
- CRUD for CMS Items (Components, Pages, etc.)
- R/O and RW operations on CMS data structures
- cmssmarteditwebservices. Read products, categories, perform synchronization
- smarteditwebservices. Configuration, languages
- smartedit. /smartedit web module
- smarteditaddon. An adapter to Accelerator. SmartEdit Design Contract implementation for the OOTB accelerator.
- ysmarteditmodule. Template for your own smartedit extension.
CMS Components
Smartedit is compatible with SAP Commerce CMS Components. As usual, for a new component, you need to register a component type extending AbstractCMSComponent or its childs, create a JSP view (<yourcomponentname.jsp>). Additionally, you need to associate a component type group with the component type:INSERT_UPDATE ComponentTypeGroups2ComponentType; source(code)[unique=true]; target(code)[unique=true]
; wide ; YourCustomComponent
; wide ; YourCustomComponent
Smartedit UI Customization
You can extend the services and features by adding or replacing the OOTB code with your own. As a starting point, create your extension using the ysmarteditmodule template. It is very likely that your code will leverage the services listed above and AngularJS built-in capabilities. So, understanding the details of both is crucial to success. The deprecated ycmssmartedit extension is not used anymore. There are three things you can customize easily:- Decorators. A decorator is a feature that adds rendering and behavior to components, which are elements in the storefront that perform different functions. For example, if some of your components are not rectangular, you need to override the default decorator to show a complex shape. A decorator is attached to a component within a page and provides HTML-rendering, DOM-manipulation, or REST API calls around its component. As a module developer, you can create decorators and wire them to different components.
- Contextual Menu Items. A contextual menu is a built-in SmartEdit decorator that contains a set of menu items, each of which performs a different function. In the following example, ICON and a submenu were added using a Contextual Menu Service.
- Toolbar Actions. Buttons or links integrated to the Smartedit header. Can be added to the header, experience, and perspective toolbars in theSmartEditUI. In the following example, A NEW BUTTON is added using the Toolbar Service.
Custom toolbar actions
In order to extend the toolbars, you need to use featureService.addToolbarItem, and register it in the perspectiveService (sorry for no indents):import {IFeatureService, IPerspectiveService, SeModule} from 'smarteditcommons';
import {doImport} from './forcedImports';
doImport();
@SeModule({
imports: [
'smarteditServicesModule',
'trainingsmarteditPerspectiveToolbarItemModule',
'MyServiceModule'
],
initialize: (perspectiveService: IPerspectiveService, featureService: IFeatureService, myService: any) => {
'ngInject';
featureService.addToolbarItem({
toolbarId: 'smartEditPerspectiveToolbar',
key: 'trainingsmarteditPerspectiveToolbarItem',
type: 'HYBRID_ACTION',
nameI18nKey: 'A New Button',
priority: 2,
section: 'left',
iconClassName: 'hyicon hyicon-info se-toolbar-menu-ddlb--button__icon',
include: 'trainingsmarteditPerspectiveToolbarItemWrapperTemplate.html',
callback: () => {
myService.openAWizard();
},
});
perspectiveService.register({
key: 'se.cms.perspective.advanced',
nameI18nKey: 'se.cms.perspective.advanced',
descriptionI18nKey: 'se.hotkey.tooltip',
features: ['trainingsmarteditPerspectiveToolbarItem'],
perspectives: []
});
}
})
export class TrainingsmarteditContainer {}
import {doImport} from './forcedImports';
doImport();
@SeModule({
imports: [
'smarteditServicesModule',
'trainingsmarteditPerspectiveToolbarItemModule',
'MyServiceModule'
],
initialize: (perspectiveService: IPerspectiveService, featureService: IFeatureService, myService: any) => {
'ngInject';
featureService.addToolbarItem({
toolbarId: 'smartEditPerspectiveToolbar',
key: 'trainingsmarteditPerspectiveToolbarItem',
type: 'HYBRID_ACTION',
nameI18nKey: 'A New Button',
priority: 2,
section: 'left',
iconClassName: 'hyicon hyicon-info se-toolbar-menu-ddlb--button__icon',
include: 'trainingsmarteditPerspectiveToolbarItemWrapperTemplate.html',
callback: () => {
myService.openAWizard();
},
});
perspectiveService.register({
key: 'se.cms.perspective.advanced',
nameI18nKey: 'se.cms.perspective.advanced',
descriptionI18nKey: 'se.hotkey.tooltip',
features: ['trainingsmarteditPerspectiveToolbarItem'],
perspectives: []
});
}
})
export class TrainingsmarteditContainer {}
//...
callback: () => {
myService.openAWizard();
},
callback: () => {
myService.openAWizard();
},
(function() {
angular.module('MyServiceModule', [
…
])
.service('myService', function(modalWizard, catalogService, pageFacade) {
this.openAWizard = function openAWizard(pageData) {
var promise = pageData ? catalogService.retrieveUriContext() :
pageFacade.retrievePageUriContext();
return promise.then(function(uriContext) {
return modalWizard.open({
controller: 'myWizardController’,
controllerAs: 'myWizardCtrl’,
properties: {
uriContext: uriContext,
basePageUUID: pageData ? pageData.uuid : undefined
}
});
});
};
})
.controller('myWizardController', function() {
this.getWizardConfig = function() {
var wizardConfig = {
steps: [{
id: "step1", name: 'Step1’,
title: 'Step’, templateUrl: 'step1.html’
}]
};
return wizardConfig;
}.bind(this);
});
})();
angular.module('MyServiceModule', [
…
])
.service('myService', function(modalWizard, catalogService, pageFacade) {
this.openAWizard = function openAWizard(pageData) {
var promise = pageData ? catalogService.retrieveUriContext() :
pageFacade.retrievePageUriContext();
return promise.then(function(uriContext) {
return modalWizard.open({
controller: 'myWizardController’,
controllerAs: 'myWizardCtrl’,
properties: {
uriContext: uriContext,
basePageUUID: pageData ? pageData.uuid : undefined
}
});
});
};
})
.controller('myWizardController', function() {
this.getWizardConfig = function() {
var wizardConfig = {
steps: [{
id: "step1", name: 'Step1’,
title: 'Step’, templateUrl: 'step1.html’
}]
};
return wizardConfig;
}.bind(this);
});
})();
Contextual Menu Items
A contextual menu is a decorator that wraps around a component. It can contain a set of icons, each of which performs a different action. You can add your own controls to the menu.featureService.addContextualMenuButton({
key: "my.menu.item.key",
i18nKey: ‘My Menu Item',
nameI18nKey: 'my.menu.item',
regexpKeys: ['MyTestComponent3’], //Regexp, here is a
condition: (config: any) => { //component uid
return true;
},
action: {
template: 'test – will be displayed below the icon'
},
displayClass: "movebutton",
iconIdle: '/trainingsmartedit/icons/icon_off.png',
iconNonIdle: '/trainingsmartedit/icons/icon_on.png',
smallicon: '/trainingsmartedit/icons/info.png',=
// permissions: ['se.read.page’], // for permissions
} as IContextualMenuButton);
featureService.enable(' my.menu.item.key ');
key: "my.menu.item.key",
i18nKey: ‘My Menu Item',
nameI18nKey: 'my.menu.item',
regexpKeys: ['MyTestComponent3’], //Regexp, here is a
condition: (config: any) => { //component uid
return true;
},
action: {
template: 'test – will be displayed below the icon'
},
displayClass: "movebutton",
iconIdle: '/trainingsmartedit/icons/icon_off.png',
iconNonIdle: '/trainingsmartedit/icons/icon_on.png',
smallicon: '/trainingsmartedit/icons/info.png',=
// permissions: ['se.read.page’], // for permissions
} as IContextualMenuButton);
featureService.enable(' my.menu.item.key ');
Homepage links
After analyzing the customization approach and available toolset, I decided to experiment with making changes in the UI outside the documented scope. Such as adding a link to a Smartedit homepage. For such operations, you need to clone the simple module from cmssmartedit and extend it:- …smarteditContainer.js: import catalogVersionDetailsModule
- Clone catalogVersionDetailsModule from cmssmartedit
- Add
this.addItems([{ include: 'myCustomCVDTemplate.html' }], CATALOG_DETAILS_COLUMNS.LEFT);
- Create myCustomCVDTemplate and add a link component
SmartEdit builder
SmartEdit uses Grunt Task Runner and its plugins to provide a single, unified set of commands for easier and faster development. Using the Grunt scripts as a black box works well if you don’t make mistakes. Without the understanding of what is behind the scripts, troubleshooting will be rather difficult. For example, all your template filenames must end with “Template”. You can read that in the documentation, but… If you forget about that, your template file won’t be converted into a javascript code during the build process, and Angular components will throw an exception. Almost all error messages are not informative in AngularJS/SmartEdit, and even in such a simple case, you can waste a day trying to find a root cause. The builder scans for the templates, and the folders and filename patterns are listed in the grunt scripts. It is very useful to read them over and understand the builder logic in detail. It will save a lot of your time later.Conclusion
Summary
- With the latest release, Smartedit has taken a definite turn for the better.
- starting from the version 1811, we’re currently out of options. Smartedit it the only tool for content management and personalization from the Commerce suite. WCMS Cockpit and BTG were removed as well as other cockpits.
- The majority of hybris developers have a lack of skills and experience. No training from SAP is provided. Documentation is good but partly outdated.
- Asynchronous personalization calculation helps to keep performance under control.
- You need an external tool to provide segments for the customers. SAP Marketing, Context-Driven Services. You can integrate a third-party or your own tool as well.