Page Designer

Before merchants create Page Designer pages in the visual editor in Business Manager, developers must create page types and component types for them to work with.

Only existing customers can access some of the links on this page. Visit Salesforce Commerce Cloud GitHub Repositories and Access for information about how to get access to the Commerce Cloud repositories.

Tip

Developing for Page Designer 

Developers play an important role in implementing Page Designer. Developers work with the ecommerce marketing director to determine the pages, components, layouts, and assets required for the storefront. The developer then creates and uploads custom cartridges that contain reusable page types, component types, and artifacts for the visual editor that help merchants quickly create and implement custom pages.

What Is Page Designer? 

Page Designer is a B2C Commerce feature that supports standard development processes and tools for creating reusable page types and component types. Merchants can use a visual editor in Business Manager to design, schedule, and publish custom pages in the storefront by dragging the page and component types. Merchants can use the same page and component types to create a variety of pages independently, without going back to the developer for updates. This topic applies to B2C Commerce.

The typical Page Designer implementation process includes these steps.

  • The ecommerce marketing director meets with the developer to discuss the pages, components, layouts, and assets required for the storefront.
  • The developer creates the reusable page types and component types that support the ecommerce marketing director's requirements.
  • The developer uploads the custom cartridge that contains the page and component types and another custom cartridge that contains supporting UI artifacts to a site.
  • The merchant uses the Page Designer visual editor in Business Manager to build storefront pages using the page and component types that the developer has implemented.

Get the Page Designer Reference Examples 

We provide example page types and component types to help you quickly get started using Page Designer.

  1. If you don’t have a GitHub account, seeSalesforce Commerce Cloud GitHub Repositories and Access .

  2. Search for the storefront-reference-architecture or page-designer-reference repository.

    • Storefront Reference Architecture (SFRA) includes Page Designer page types and component types optimized for use with SFRA. The latest version of SFRA is in the SFRA repository.
    • Page Designer reference cartridges for Site Genesis are in the SiteGenesis repository.
  3. Click Clone or download to copy the repository to your local system.

  4. If you downloaded the repository, expand the .zip file.

    On macOS, you can use the command-line unzip utility to expand the files.

    Note

    For information about how to get started with the SiteGenesis page-designer-reference repository, see Site Genesis README.

    For details about the Page Designer example pages and components for the SiteGenesis page-designer-reference repository, see Site Genesis Implementation Guide.

Page Designer Development Best Practices 

In addition to following standard best practices for B2C Commerce development, follow these best practices that are specific to Page Designer.

  • Do as much planning and gathering of requirements upfront as you can with the merchant and ecommerce director before developing page types and component types.

  • Create components that are self-contained so that any region where they’re placed can handle them as “black boxes.” To support self-containment, give each component its own default styling, although at times you might need to adjust the styling by specifying custom render settings.

  • If you need to change the meta definition for a page or component type after the merchant has already created instances of pages and components, be careful not to make incompatible changes. For example, if you change the type value for a component attibute, you create an inconsistency between the component type meta definition and the component data in the database. In this case, it's better to create a new component type and deprecate the old one.

  • For sites that share libraries, assign the same data and the same set of cartridges with the same set of page and component types to each site. Sharing data and code ensures that there aren’t inconsistencies between meta definitions and data. For example, you could have a page that was known in one site but not another, or two sites that use the same ID for different component types with different sets of attributes, which would result in validation errors. Keep in mind that data and code are shared using different mechanisms that might have multiple relationships to sites, as follows:

    EntityCan Be Assigned To
    Cartridge1...n sites
    Library1...n sites
    Conent Asset1 library
    Page1 library
    Component1 library
    Catalog1...n sites
  • Implement anchor tags on your Page Designer pages.

    For example, to build a page and anchor to each respective section of the page, or potentially a component at the top with images or links that would anchor below them, do the following:

    1. In each component use a string attribute that is used to define a tag.

      <a name="string_attribute_value"/>

    2. When creating the links, reference the tag.

      <a href="#string_attribute_value">Link to the string_attribute_value section</a>

  • Dynamically pull a profile attribute into a Page Designer component.

    For example, to display firstName or companyName in a component on the homepage after a customer logs in, such as "Welcome back, {firstName}", use:

    Request.getSession().getCustomer() .

    Be sure it isn't cached.

    Note

  • Duplicate a Page Designer page to a new library.

    Using the Shared Library Page Designer, pages are automatically duplicated within the shared library. They’re also created offline allowing the merchandiser to determine if the page is available within the other locations.

  • Caching for Page Designer pages is similar to other pages. For more information, see Page Caching.

  • Check the Page Designer page cache time to live.

    There’s no Page Designer-owned cache time to live, only what the customer developers set in their scripts. Use the "cache information" tool in SFTK to check how long a given page is cached. Select the top or page level cache icon, and look for a remote include of the pipeline "Internal - Page Designer".

Page Designer General Troubleshooting 

The following tips have been found to quickly discover and help solve many common errors.

  • Refer to the browser developer console to determine if there are errors shown.

    • Check for general javascript errors, induced by either the storefront itself or Page Designer.
    • Check the network tab in the browser console and monitor the XHR requests (for example, you can see any API request going back and forth).
  • Refer to the server logs to determine if there are errors shown.

    • In Business Manager, go to Administration > Site Development > Development Setup.
    • Browse the log files checking especially the error logs for any related entries, such as script exceptions.
  • Work within Page Designer Optimal Performance.

    Page Designer Pages within Business Manager aren’t cached allowing Content Editors and Merchandisers to see their updates immediately. For optimal performance, we recommend you ensure the nesting within components are kept to 5 or under.

  • If you’re experiencing a 'Service Unavailable Error - 500', this is not a Page Designer specific issue. Check the error logs to determine the issue.

Optimal Page Performance 

Page Caching is skipped when a page is viewed in either editing or previewing mode of the Page Designer tool, so any change can be immediately visible to the user. However in the plain storefront, the customer page cache instrumentations apply as coded. Slow rendering typically results from either the page not yet in the page cache when viewed outside of Page Designer, or a low-performing page implementation.

For slow performing pages, we recommend you check to see if the implementation has to be revised:

  • in the storefront, use the STFK.
  • enable the Cache Info tool.
  • look for the cache symbol for pipeline “Internal - Page Designer”, and check its caching status.

For uncached occurrences that must be revisited for an implementation change, we recommend you:

  • use page caching unless it’s ultimately necessary to not cache the given page. See Page Caching for more information.
  • refactor the business logic of the page avoiding costly operations, or mass amounts of operations, during one request.

Create Page and Component Types 

To implement page types and component types, you must include a meta definition file and a script file for each page type and component type in a custom cartridge.

Page Designer Meta Definition Files 

Each page type and component type requires a JSON meta defintion file. A meta definition file for a page type describes the regions of the page where a merchant can place components. A meta defintion file for a component type describes the attributes that a merchant defines when using the component type and can also define regions within the component type.

Put the meta definition file in one of the following directories of the custom cartridge, or any arbitrary subdirectory within these directories:

  • Page types: {your_cartridge}/cartridge/experience/pages
  • Component types: {your_cartridge}/cartridge/experience/components

The meta definition file name can include only alphanumeric or underscore characters. If you put the meta definition file into a subdirectory within the /experience/pages or /experience/components directories, the names of the levels in the subdirectory must also use only alphanumeric or underscore characters.

To view schema files that describe the JSON formatting for the meta definition file, see Page Designer JSON Schemas.

This example promotion page type meta definition file, named promopage.json, defines three regions for the promotions page: header, main, and footer. The header and footer regions can display only one component at a time, as specified by max_components = 1. The region_definition for the main region doesn’t include a max_components value, indicating that the main region can display any number of components. The main region uses component_type_exclusion to specify that components of type banner aren’t allowed in the main region.

The max_components value restricts the number of components rendered for a region at a time, but the region can contain any number of components. You can therefore display different components for different customers or on different schedules. For example, you could have a banner region that displays one component in winter and a different component in summer.

promopage.json
{
    "name":"Promopage",
    "description":"A promotional page.",
    "region_definitions":[
      {
        "id":"header",
        "name":"Name",
        "description":"Header of the promopage",
		"max_components":1
      },
      {
        "id":"main",
        "name":"Main",
        "description":"Main area of the promopage",
        "component_type_exclusions":[
           {"type_id":"banner"}
        ]
      },
      {
        "id":"footer",
        "name":"Footer",
        "description":"Footer of the promopage",
        "max_components":1
      }
    ]
}

This example banner component type meta definition file, named banner.json, defines four attributes that the merchant can set: the image file, its alt text, its size, and an attribute named Magic Unicorns that lets the merchant select which unicorn to display on the banner. The Magic Unicorns attribute is configured using a custom UI control, com.sfcc.magical. In the Page Designer visual editor, the attributes appear in groups. For example, the four attributes defined in this file appear in the visual editor in a group labeled Banner Image Configuration.

The default_value setting for an attribute is used only for rendering the page in the storefront or previewing in the visual editor. The default_value isn’t shown as preselected in the visual editor that the merchant uses to configure the component. Rendering behavior based on settings for required and default_value is as follows:

  • Required is true and a default_value is configured: The default_value is assigned to the attribute for rendering the page, but isn’t shown as preselected in the visual editor. If the merchant doesn’t configure a value for the attribute and saves the component, no error message is presented.
  • Required is false and a default_value is configured: The default_value is assigned to the attribute for rendering the page, but isn’t shown as preselected in the visual editor. If the merchant doesn’t configure a value for the attribute and saves the component, no error message is presented.
  • Required is true and no default_value is configured: If the merchant doesn’t configure a value for the attribute and saves the component, an error message is presented.
  • Required is false and no default_value is configured: If the merchant doesn’t configure a value for the attribute and saves the component, no error message is presented.
banner.json
{
  "name": "Banner",
  "description": "A banner.",
  "group": "content",
  "attribute_definition_groups": [
    {
      "id": "image",
      "name": "Banner Image Configuration",
      "description": "You can define the image file, size and alt text for the banner image.",
      "attribute_definitions": [
        {
          "id": "image",
          "name": "Banner Image",
          "description": "The image shown by the banner.",
          "type": "file",
          "required": true
        },
        {
          "id": "alt",
          "name": "Banner Image Alt Text",
          "description": "The image alt text shown by the banner.",
          "type": "string",
          "required": false
        },
        {
          "id": "size",
          "name": "Size",
          "description": "The banner size.",
          "type": "enum",
          "values": [
            "small",
            "medium",
            "large"
          ],
          "required": true,
          "default_value": "medium"
        },
        {
          "id": "magic",
          "name": "Magic Unicorns",
          "type": "custom",
          "required": true,
          "editor_definition": {
            "type": "com.sfcc.magical",
            "configuration": {
              "options": {
                "config": [
                  "Auris",
                  "Chant",
                  "Elmas",
                  "Majesty",
                  "Moriba",
                  "Mystery",
                  "Mystic",
                  "Snowflake",
                  "Solstice",
                  "Sunshine"
                ]
              }
            }
          }
        }
      ],
      "region_definitions": []
    }
  ]
}

The 3column.json meta definition file is for a component type that represents a layout of three columns. Each column is a region in the meta definition file.

3column.json
{
  "name": "1 Row, 3 Column, Landscape",
  "group": "layouts",
  "attribute_definition_groups": [],
  "region_definitions": [
      {
        "id":"column1",
        "name":"Column 1",
        "max_components":1
      },
      {
        "id":"column2",
        "name":"Column 2",
        "max_components":1
      },
      {
        "id":"column3",
        "name":"Column 3",
        "max_components":1
      }
  ]
}

Page-Level Custom Attributes 

You can define custom page-level attributes for a page type. For example, you can define custom attributes for title, Image, and Description. Merchants can use those attributes to specify the image and text that appears in the search results for a page.

Define the page-level attributes in the meta definition file for the page type. This example shows three attributes defined for a Search Results Title, Search Results Image, and Search Results Description.

promopage.json
{
	"name": "Promopage",
	"description": "A promotional page.",

	"attribute_definition_groups": [{
		"id": "search",
		"name": "Search Results Display",
		"description": "Define the title, image, and description returned in search results for the page.",
		"attribute_definitions": [{
				"id": "title",
				"name": "Search Results Title",
				"description": "The page title displayed in search results.",
				"type": "text",
				"required": false
			},
			{
				"id": "image",
				"name": "Search Results Image",
				"description": "The image displayed in search results.",
				"type": "file",
				"required": false
			},
			{
				"id": "description",
				"name": "Search Results Description",
				"description": "The page description displayed in search results.",
				"type": "text",
				"required": false,
			}
		],
		"region_definitions": [{
				"id": "header",
				"name": "Name",
				"description": "Header of the promopage",
				"max_components": 1
			},
			{
				"id": "main",
				"name": "Main",
				"description": "Main area of the promopage",
				"component_type_exclusions": [{
					"type_id": "banner"
				}]
			},
			{
				"id": "footer",
				"name": "Footer",
				"description": "Footer of the promopage",
				"max_components": 1
			}
		]
	}]
}

Initialize a Region with a Component Already Populated 

Prepopulating a region with default components is a best practice that guides a merchant in creating a page. To specify that a region contains specific components by default, use the default_component_constructors property in the meta definition file for a page type. You can also use this property in the meta definition file for a component type that includes a region. When the merchant creates a page in the Visual Editor using the page type or component type, the default components are already populated in the region. The merchant can delete the components if necessary.

The following example specifies that the main region of the page is prepopulated with a default component. The component has id commerce_layouts.productDetailand name Product Detail Layout.

{
  "name": "Product Detail Page",
  "description": "Product detail page with 3 regions",
  "region_definitions": [
    {
      "id": "top",
      "name": "Top Region",
      "component_type_exclusions": [{ "type_id": "commerce_assets.campaignBanner" }]
    },
    {
      "id": "main",
      "name": "Main Region",
      "default_component_constructors": [
        { "type_id": "commerce_layouts.productDetail", "name": "Product Detail Layout" }
      ],
      "component_type_exclusions": [{ "type_id": "commerce_assets.campaignBanner" }]
    },
    {
      "id": "bottom",
      "name": "Bottom Region",
      "component_type_exclusions": [{ "type_id": "commerce_assets.campaignBanner" }]
    }
  ],
  "supported_aspect_types": ["pdp"]
}

Page Designer Script Files 

Each page type or component type requires a corresponding script file. The script file must have the same name as the type's meta definition file, but with a .js extension. For example, if the meta definition file for a promotions page type is promopage.json, the script file is named promopage.js.

The script file name can include only alphanumeric or underscore characters. Put the script file at the same location as its corresponding meta definition file. For example, for a component type named headlinebanner, you could include the meta definition file and the script file at the following locations :

mycartridge/cartridge/experience/components/assets/headlinebanner.json

mycartridge/cartridge/experience/components/assets/headlinebanner.js

The script file must include a render function that returns the markup for the page. You can assemble the markup using any process you want, as long as the result is a string. In many cases, the render function calls an ISML template to which it passes information about the page or component type and its content. If you use an ISML template, you must use the dw.util.Template API to render the markup from the template. Don’t use dw.template.ISML because it doesn't return a string, and it writes the markup to the response stream right away.

In this promotion page script file, named promopage.js, the context object that is passed to the render function is of type dw.experience.PageScriptContext. It provides access to:

  • context.page—Currently rendered page
  • context.renderParameters—Parameters passed to PageMgr.renderPage(pageID, parameters)
  • context.content—Attributes set in the custom logic (not defined by the merchant)
promopage.js
'use strict';
var Template = require('dw/util/Template');
var HashMap = require('dw/util/HashMap');

/**
 * Render logic for the page.
 */
module.exports.render = function (context) {
    var model = new HashMap();
    // add paramters to model as required for rendering based on the given context (dw.experience.PageScriptContext):
    //   * context.page (dw.experience.Page)
    //   * context.renderParameters (String)
    //   * context.content (dw.util.Map)

    return new Template('experience/pages/promopage').render(model).text;
};

In this banner script file, named banner.js, the context object that is passed to the render function is of type dw.experience.ComponentScriptContext. It provides access to:

  • context.component—The currently rendered component.
  • context.componentRenderSettings—Render settings provided by the hosting region. You can optionally override these render settings that are passed in from the region with settings specific to the component.
  • context.content—Attributes as defined in the meta definition file for the component and then configured by the merchant in the Page Designer visual editor, or as defined in the custom logic.
banner.js
'use strict';
var Template = require('dw/util/Template');
var HashMap = require('dw/util/HashMap');

/**
 * Render logic for the component.
 */
module.exports.render = function (context) {
    var model = new HashMap();
    // add paramters to model as required for rendering based on the given context (dw.experience.ComponentScriptContext):
    //   * context.component (dw.experience.Component)
    //   * context.content (dw.util.Map)

    return new Template('experience/assets/banner').render(model).text;
};

Component Attribute Types and UI Controls 

Each attribute in the meta definition file for a component type has a type assigned to it. The attribute's type determines how the merchant sets its value in the Page Designer visual editor. For example, an attribute of type file displays a file selection modal window that the merchant can use to browse the library files. An attribute of type enum displays a single select box where the merchant chooses one of the allowed values.

This table describes the type options for component attributes and how those options are displayed in the Page Designer visual editor.

Component Attribute TypeAttribute SemanticsVisual Editor UI Control
booleanBooleanCheck box
categoryString representing a catalog category IDCategory picker
cms_recordRecord from the Salesforce CMS (Content Management System)User is presented with a modal window to select the CMS record. After the user selects the CMS record, the edit panel displays the UI controls necessary to configure the attributes of the slected CMS record.
customString enclosed in curly brackets {} that represents a JSON objectText area or custom UI control
enumEnumeration of either string or integer values.Select box (single select)
fileString representing a file path within a libraryFile picker
imageString representing a configurable image JSONImage picker that lets users select an image and specify a focal point on that image. The image dimensions are also stored and can be accessed, along with the image name and focal point, using the Image API.
integerIntegerInput field
markupString representing HTML markupA rich text editor that allows semantic formatting options to produce HTML markup
pageString representing a page IDPage picker
productString representing a product SKUProduct picker
stringStringInput field
textStringText area
urlString representing a URLURL picker
imageString representing a configurable image JSONImage picker that lets users select an image and specify a focal point on that image. The image dimensions are also stored and can be accessed, along with the image name and focal point, using the Image API

Component Attribute Types and Resolved Value Objects 

The content attributes available from the context object, also known as the content dictionary, are defined in a map with the attribute IDs as the keys and the resolved values as values. A resolved value is a value that has been converted to B2C Commerce API objects when necessary. For example, for an attribute of type product, the value of the attribute (the product SKU) is converted to a dw.catalog.Product object instance. For some attributes, conversion isn't necessary. For example, strings can be stored as is. The following table lists the content attribute types and their corresponding B2C Commerce API objects.

Content Attribute TypeB2C Commerce API Value
categorydw.catalog.Category
cms_recorddw.experience.CMSRecord
customdw.util.Map
filedw.content.MediaFile
imagedw.experience.image.Image
pagedw.experience.Page
productdw.catalog.Product

Page Designer HTML 

The render function of a page type creates the page's HTML markup. Typically, the page type's render function calls an ISML template that uses PageMgr.renderRegion() to render each region of the page.

In this example, a page calls an ISML template. This ISML template renders three regions: header, main, and footer.

promopage.isml
<html>
  ...
  <isprint value="${PageMgr.renderRegion( pdict.page.getRegion('header') )}" encoding="off" />
  ...
  <isprint value="${PageMgr.renderRegion( pdict.page.getRegion('main') )}" encoding="off" />
  ...
  <isprint value="${PageMgr.renderRegion( pdict.page.getRegion('footer') )}" encoding="off" />
  ...
</html>

Each PageMgr.renderRegion() function in the page ISML finds all assigned and visible components in each region and calls each component’s render function, which in turn calls its own ISML template, as in the following example:

banner.isml
...
<img
  width="<!-- calculate it in your component controller using ${pdict.content.size} -->"
  src="${pdict.content.image.getAbsURL()}"
  alt="${pdict.content.alt}"
/>
<!--
   // if the 'assets.banner' component type had a region 'foobar' we could render it here
   <isprint value="${PageMgr.renderRegion( pdict.component.getRegion('foobar') )}" encoding="off"/>
-->
...

As each region and component is rendered, Page Designer creates an HTML element to wrap the content. You can specify which HTML element to use as the wrapper and which attributes to assign to the wrapper using the dw.experience.RegionRenderSettings and dw.experience.ComponentRenderSettings APIs. For more information, see:

By default, the HTML wrapper for regions and components is div. The default CSS class for regions is experience-region experience-<{region_definition_id}>. The default CSS class for components is experience-component experience-<{componenttype_id}>.

Component type ID values include dots, for example, assets.productile. When used in the CSS class name, the dot is replaced with a hyphen, for example, experience-component experience-assets-productile.

Note

For example, for a region with id my_region and three components, two of type my_component_type_1 and one of type my_component_type_2, the default HTML wrapper is:

Default Render Settings
<div class="experience-region experience-my_region">
  <div class="experience-component experience-my_component_type_1">
    content of first component (type my_component_type_1) goes here
  </div>
  <div class="experience-component experience-my_component_type_1">
    content of second component (type my_component_type_1) goes here
  </div>
  <div class="experience-component experience-my_component_type_2">
    content of third component (type my_component_type_2) goes here
  </div>
</div>

In some cases, you want a component to have a different style depending on which region contains the component. For example, you define a component type for a product tile that uses default render settings. You define another component type for a carousel, which is a one-region component that scrolls through a number of other components. When the product tile component appears in the carousel, you want it to use different styling specific to the carousel.

You could specify that the HTML wrapper for the carousel uses a custom CSS class called item for the first and second components. Then specify a different CSS class, item active, for the third component, to distinguish it as the active component in the carousel. Here's the HTML wrapper for the carousel.

<div class="carousel-inner">
  <div class="item">content of first component (type my_component_type_1) goes here</div>
  <div class="item">content of second component (type my_component_type_1) goes here</div>
  <div class="item active">content of third component (type my_component_type_2) goes here</div>
</div>

Here's the code to set the custom render settings for the carousel. You could include this code in the render function of the script file for the component.

The Page Designer reference implementation available in GitHub includes helper functionality to streamline this code.

Note

var carousel = component.getRegion("carousel");

// set styling for carousel region
var carouselRenderSettings = new dw.experience.RegionRenderSettings();
carouselRenderSettings.setTagName("div");
carouselRenderSettings.setAttributes({ class: "carousel-inner" });

// set carousel item styling (default)
var defaultCarouselComponentRenderSettings = new dw.experience.ComponentRenderSettings();
defaultCarouselComponentRenderSettings.setTagName("div");
defaultCarouselComponentRenderSettings.setAttributes({ class: "item" });
carouselRenderSettings.setDefaultComponentRenderSettings(defaultCarouselComponentRenderSettings);

// set carousel item styling (active one)
var activeCarouselComponent = carousel.visibleComponents[0]; // init the first one as being active
var activeCarouselComponentRenderSettings = new dw.experience.ComponentRenderSettings();
activeCarouselComponentRenderSettings.setTagName("div");
activeCarouselComponentRenderSettings.setAttributes({ class: "item active" });
carouselRenderSettings.setComponentRenderSettings(
  activeCarouselComponent,
  activeCarouselComponentRenderSettings,
);

// render the carousel region
return dw.experience.PageMgr.renderRegion(carousel, carouselRenderSettings);

Page Type, Component Type, and Custom Attribute Editor IDs 

A page type, component type, or custom attribute editor ID is a combination of the subdirectory (if any) where the meta definition file and script file are located and the file name without the extension. Separate the levels of the subdirectory and the file name with periods. For example, for a page type named homepage with a meta definition file and script file located directly under cartridge/experience/pages, the page type ID is homepage. If the meta definition file and script file for homepage are located in a subdirectory storefront/campaigns under cartridge/experience/pages, the page type ID is storefront.campaigns.homepage.

Length Restrictions for Page Type and Component Type IDs 

When component types and page types are persisted in the database, their IDs are prepended with the prefixes page. or component.. Those prefixes, plus the IDs of the page types and component types, can’t exceed 256 characters, or else errors occur. Adjust the file names or move the meta definition file and script file to a higher level in the directory structure to shorten the ID.

Location of Page Type, Component Type, or Custom ControlID
mycartridge/cartridge/experience/components/assets/headlinebanner.jsonassets.headlinebanner
mycartridge/cartridge/experience/components/assets/banners/headlinebanner.jsonassets.banners.headlinebanner
mycartridge/cartridge/experience/pages/promopage.jsonpromopage
mycartridge/cartridge/experience/pages/promotions/promopage.jsonpromotions.promopage
mycartridge/cartridge/experience/editors/com/sfcc/magical.jsoncom.sfcc.magical

Using Decorators with Page Designer Pages 

You can implement various strategies for using decorators with Page Designer pages. For example, you can write the script file for a page type so that you can pass in a custom decorator as a parameter when the page is rendered, or fall back to a default decorator defined in the script. You can also write the controller that renders the page so that a different decorator is used based on the value of a certain condition.

In the following snippet from a page script file, the constant DEFAULT_DECORATOR defines the location of the default decorator, which the rendering process uses if no other decorator is supplied. If the location of a different decorator is supplied as a parameter when rendering the page, that decorator is used instead. The following line of this example directs the rendering process to use the decorator parameter passed in to the rendering process, if one exists, and if no decorator parameter was passed in, use the DEFAULT_DECORATOR.

model.decorator = renderParameters.decorator || DEFAULT_DECORATOR;

...
const DEFAULT_DECORATOR = 'decoration/styling';
...
module.exports.render = function (context) {
   var model = new HashMap();
   ...
   var renderParameters = getRenderParameters(context.renderParameters);
   model.decorator = renderParameters.decorator || DEFAULT_DECORATOR;
   return new Template('experience/pages/dynamiclayout').render(model).text;
}

When pages are displayed in the visual editor as the merchant is creating them, the default decorator defined in the script file is used. To optimize the experience of creating pages for the merchants, you could define the DEFAULT_DECORATOR constant in the script to point to a lightweight decorator that loads quickly in the visual editor. But then when the controller renders the page in the storefront, you could pass in the location of the full decorator to use to display the pages.

You can also choose which decorator to use based on the state of a condition. For example, in the following snippet from a storefront controller, the decorator at decoration/ajax is used if the format of the page is based on AJAX (Asynchronous Javascript and XML), but the decorator at decoration/full is used if the page isn’t based on AJAX.

exports.Show = function () {
   var params = request.httpParameterMap;
   var cid = params.cid.stringValue;
   var format = params.format.stringValue;
   ...
   var page = PageMgr.getPage(cid);
   ...
   var params = {
   decorator: format === 'ajax' ? 'decoration/ajax' || 'decoration/full';
   };
   response.writer.print(PageMgr.renderPage(page.ID, JSON.stringify(params)));
...
}

Develop a Dynamic Page 

A dynamic page uses one or more dynamic attributes passed to the page at runtime to determine what content to display in the storefront. The typical use case is the developer creates an aspect type for a particular business use, for example a Product List or Product Details page. The developer assigns the aspect type to a page type that defines the layout for the page. The merchant uses the page type to create a template Product List or Product Detail page and assigns the template page to one or more categories. Whenever a Product List or Product Detail page is required in the storefront for one of the assigned categories, the template page is displayed. The content of the template page is populated based on the category dynamic attribute.

For an example of how a merchant implements a Product List or Product Details template, see Create a Product List or Product Detail Page Template for Page Designer.

Aspect Types 

To implement a dynamic page, create an aspect type meta definition file that defines the attributes required for a particular business use. The aspect type meta definition file also defines the business object type that the merchant can bind to the page when the page is created. In the meta definition file for the page type, specify which aspect types are supported for the page type.

To view the schema file that describes the JSON formatting for the aspect type meta definition file, see Page Designer JSON Schemas.

Put the aspect type meta definition file in the following directory of the custom cartridge, or any arbitrary subdirectory within this directory:

{your_cartridge}/cartridge/experience/aspects

Valid values for supported_object_types are 'category' or 'product'.

Using 'category' allows the merchant to assign pages to a category in the Page Designer UI and developers to retrieve them using PageMgr.getPageByCategory.

Using 'product' allows the merchant to assign pages to a product in the UI and developers to retrieve them using PageMgr.getPageByProduct..

You can also create an aspect type with no supported_object_type declared. For example, you could implement a Product List page that depends on the results of a query, not the assignment of a category, to determine which products to display on the page.

The 'attribute_definitions' of the aspect type define which attributes should be passed in when rendering the page using PageMgr.renderPage.

The following examples show aspect meta definition files for a Product List page and a Product Detail page. The Product List page aspect type has one attribute for category and supports the object type category, which the merchant assigns during page creation. The Product Detail page aspect type has two attributes, one for product and one for category. The Product Detail page also supports the object type category, which the merchant assigns during page creation. All products in the assigned categories use the Product Detail page.

plp.json
{
  "name": "Product List Page",
  "description": "The product list page for a given category.",
  "attribute_definitions": [
      {
          "id": "category",
          "name": "Category",
          "type": "category"
      }
  ],
  "supported_object_types": [
      "category"
  ]
}
pdp.json
{
  "name": "Product Details Page",
  "description": "The product details page for a given product.",
  "attribute_definitions": [
      {
          "id": "product",
          "name": "Product",
          "description": "The product to render the pdp for.",
          "type": "product",
          "required": true
      },
      {
          "id": "breadcrumb_category",
          "name": "Breadcrumb Category",
          "description": "The optional category context of the pdp, for breadcrumb purposes.",
          "type": "category",
          "required": false
      }
  ],
  "supported_object_types": [
      "category"
  ]
}

Specify the supported aspect types in the page definition file for the page type, as in the following example. Because this page type declares support for the plp aspect type, the page requires the category attribute defined in the plp.json meta definition file. Category is the dynamic attribute that the controller or script uses to render the page in the storefront.

plplayout.json
{
	"name": "Product List Page Layout",
	"description": "A layout for a product list page with a region to contain a product grid component.",
	"supported_aspect_types": [
		"plp"
	],
	"region_definitions": [{
		"id": "main",
		"name": "Main Region"
	}]
}

You can create a page type that supports more than one aspect type. During page creation, the merchant selects which aspect type to use for the page by selecting a Page Purpose in the page creation wizard.

dynamiclayout.json
{
	"name": "Dynamic Layout",
	"description": "A generic layout whose region can use any kind of component.",
	"supported_aspect_types": [
		"plp",
		"pdp"
	],
	"region_definitions": [{
		"id": "main",
		"name": "Main Region"
	}]
}

Components with Dynamic Attributes 

A component attribute can include a dynamic_lookup property. The dynamic_lookup property specifies an attribute of the aspect. The component attribute gets its value from the aspect attribute specified.

In the following example, the component attribute named Category includes adynamic_lookup property. The dynamic_lookup property specifies the aspect attribute category. At runtime, component attribute Category gets its value from aspect attribute category.

productgrid.json
{
  "name": "Product Listing Layout",
  "description": "Product Listing Grid Layout",
  "group": "commerce_layouts",
  "attribute_definition_groups": [
    {
      "id": "productList",
      "name": "Settings",
      "description": "Product List Settings",
      "attribute_definitions": [
        {
          "id": "category",
          "name": "Category",
          "type": "category",
          "required": false,
          "dynamic_lookup":{
              "aspect_attribute_alias": "category"
          }
        }
      ]
    },

	"region_definitions": [...]
}

In the Visual Editor, the merchant can override the category that the page is using and assign a different category to the component. For example, in the SFRA reference implementation, the dynamic banner component uses the category value assigned to the page by default. The component uses the banner image and banner text assigned in Business Manager for the category assigned to the page.

Dynamic Banner Component showing category assigned to page

The merchant can edit the category value and assign it to something other than the value the page is using. For example, the merchant can specify that the banner component uses the category mens-clothing regardless of the category assigned to the page.

Dynamic Banner Component showing mens-clothing category

As the SFRA reference component is implemented, the merchant can also override attributes of the component separately from the dynamic category attribute. For example, the merchant can specify that regardless of the category, the banner always displays an image for women's shoes and handbags.

Dynamic Banner Component showing specific image

Render a Dynamic Page 

Aspect attributes are passed to the page through the Page.renderPage() method that takesaspectAttributes.

The following code snippet passes aspect attributes to the page during rendering:

var aspectAttributes = new HashMap();
aspectAttributes.put("category", category);
res.print(PageMgr.renderPage(page.ID, aspectAttributes, ""));

The following code snippet checks to see if a page is assigned to the given category for a given aspect type and returns that page. If there’s no page assigned, the code walks up the category hierarchy until it either finds a category with the page assigned or it reaches root. If no page is found even for root, it returns null.

var ignoreInvisiblePages = true;
var anAspectTypeID = "plp";

PageMgr.getPage(aCategory, ignoreInvisiblePages, anAspectTypeID);

Best Practices for Developing a Dynamic Page 

Follow best practices when developing a dynamic page.

  • Use the default_component_constructors property in the page meta definition file to populate a region on the page with a component. For example, specify that the main region of a Product List page contains a Product List grid component. When the merchant creates a Product List page in the Visual Editor, the main region is already populated with the Product List grid component. This approach helps the merchant to quickly create pages that use the appropriate components.
  • Be specific when naming a component, clearly indicating what kind of page the component is intended for. For example, name a component intended for a Product Detail page, Product Detail Grid. If the merchant uses a component that has attributes not specified by the aspect type assigned to the page, the component doesn't render correctly. By naming the components carefully, you can help merchants create pages with the appropriate components.
  • Be careful when creating a page type that supports more than one aspect type. Merchants must complete the extra and potentially confusing step of selecting a page type purpose during creation. Also, a page type generic enough to accommodate more than one aspect type can make only limited use of prepopulated components, providing the merchant less guidance during page design.

Create a Custom Cartridge for Page Designer UI Artifacts 

To localize page types and component types, or to display thumbnail images for them in the visual editor, create a custom cartridge that includes resource bundles for localization and thumbnail image files. Upload the custom cartridge to the Business Manager site.

Create Page Designer Localization Resource Bundles 

A resource bundle contains the localized names and descriptions for each page type, component type, and component type group that you want to localize. Including a resource bundle is optional. If you don’t provide them, the visual editor uses information in the meta definition files.

Each page type, component type group, and component type requires a separate resource bundle.

Name the resource bundle files using this pattern:

{componenttype/pagetype/componenttypegroup}__{locale}_.properties

Put the resource bundles for page types and component types in this location in the cartridge:

{your_cartridge}/cartridge/templates/resources/experience/pages

{your_cartridge}/cartridge/templates/resources/experience/components

Put resource bundles for component type groups within a componentgroup subdirectory: {your_cartridge}/cartridge/templates/resources/experience/componentgroups

If the page type and component type files are located in a subdirectory of the ../experience/pages or ../experience/components directories, the resource bundles must be located in a corresponding subdirectory of the resources directory.

Page Type, Component Type, Component GroupResource Bundle in Cartridge
mycartridge/cartridge/experience/components/banners/headlinebanner.jsonmyBMcartridge/cartridge/templates/resources/experience/components/banners/headlinebanner_de_DE.properties
mycartridge/cartridge/experience/pages/promopage.jsonmyBMcartridge/cartridge/templates/resources/experience/pages/promopage_fr.properties
mycartridge/cartridge/experience/pages/promotions/promopage.jsonmyBMcartridge/cartridge/templates/resources/experience/pages/promotions/promopage_en.properties
contentmyBMcartrige/cartridge/templates/resources/experience/componentgroups/content_en.properties

Example Resource Bundle for a Page Type 

promopage_en.properties
name=Promotional Page
description=A promotional page with header, main area and footer.
region.header.name=Header
region.main.name=Main
region.footer.name=Footer

Example Resource Bundle for a Component Type 

banner_en.properties
name=Banner
description=A banner that allows to configure which image to show in a sizable manner.
attribute_definition_group.image.name=Banner Image Configuration
attribute_definition_group.image.description=You can define the image file, size and alt text for the banner image.
attribute_definition.image.name=Image
attribute_definition.image.description=The image shown by the banner.
attribute_definition.alt.name=Image Alt Text
attribute_definition.alt.description=The image alt text.
attribute_definition.size.name=Size
attribute_definition.size.description=The banner size.

Example Resource Bundle for a Component Type Group 

The resource bundle for a component type group contains only the name value of the group.

content_en.properties
name=Content

Page Designer Thumbnail Images 

In the Page Designer visual editor, thumbnail images assist the merchant in determining which page or component to select. If you don't provide thumbnail images, generic images are displayed.

The file name for a thumbnail images (without the extension) must match the name of the respective page type or component type, for example, promopage.png or promopage.svg. Page Designer doesn't support a different thumbnail image for different locales.

Thumbnail Images for Pages 

Put thumbnail images for page types in this location.

{your_cartridge}/cartridge/static/default/images/experience/pages/{subdirectory}/{id}

The optional {subdirectory} must match the subdirectory where the pages are in the ../experience/pages directory.

For example if the page type is located in mycartridge/cartridge/experience/pages/promotions/promopage.json, the image file must be located here:

myBMcartridge/cartridge/static/default/images/experience/pages/promotions/promopage.png

Thumbnail Images for Components 

Put thumbnail images for component types at this location in your cartridge:

{your_cartridge}/cartridge/static/default/experience/components/{sudirectory}/{id}

The optional {subdirectory} must match the subdirectory where the component types are in the ../experience/components directory.

For example if the component type is located in mycartridge/cartridge/experience/components/banners/headlinebanner.json, the image file must be located here:

mycartridge/cartridge/static/default/experience/components/banners/headlinebanner.png

Use Salesforce CMS Content with Page Designer 

Use Salesforce CMS to manage all your content in a single repository. Merchants can use the CMS content on a Page Designer page. For example, you develop a component type that displays a headline banner. You can create and manage different options for the content of the headline banner in Salesforce CMS. When merchants add the component type to a page, they select which content to use for the headline banner from Salesforce CMS.

Get Access to Salesforce CMS 

To use CMS content with Page Designer components, make sure your Salesforce org includes Salesforce CMS.

Contact your Salesforce Account Executive or Salesforce Commerce Cloud Success Manager for information about pricing and packaging. For information about Salesforce CMS, see Salesforce Content Management System (CMS).

Connect Salesforce CMS to Page Designer 

Connect your B2C Commerce instance to a CMS channel and assign the channel to your site library.

  1. Open a support ticket and request a trust relationship between your Salesforce org and your B2C Commerce instance.

  2. In Salesforce CMS, add a CMS workspace to host the Page Designer content or use an existing CMS workspace.

    For information about creating CMS workspaces, see Create a CMS Workspace in Salesforce CMS.

    Add CMS workspace

  3. In Salesforce CMS, create a Commerce Cloud channel.

    For information about creating a CMS channel, see Create a CMS Channel.

    Create CMS channel

  4. In the CMS workspace, add the Commerce Cloud channel.

    For information about adding CMS channels, see Add a Channel to a CMS Workspace.

    Add channel

  5. In B2C Commerce Business Manager, go to Administration > Sites > Content Libraries > site > General. Scroll to the bottom and assign the CMS channel to your site library.

    Assign channel to site library

Develop a Component Type to Use CMS Content 

Define a component type with an attribute assigned to Salesforce CMS content.

  1. Determine the content required to configure the compontent type attribute.

    For example, you want merchants to be able to include a headline banner on their Page Designer page by selecting a CMS record. You can use the news content type, which already exists in Salesforce CMS, to create and manage the content for the headline banner.

    If the content type you want to use in the Page Designer page doesn't already exist in the CMS, create it. See Create Custom Content Types for Salesforce CMS.

  2. Create a component type that includes attibutes assigned to type cms_record, with the content_type specified in the editor_definition element.

    For example, the following meta defintion file and script files for component type CMS Headline Banner use a Headline Content attribute of type cms_record, with content_type news.

    cmsheadlinebanner.json
    {
        "name": "CMS Headline Banner",
        "description": "Image, text overlay from CMS that links user to a B2C target using the markup editor",
        "group": "assets",
        "attribute_definition_groups": [{
                "id": "headlineContent",
                "name": "Headline Content",
                "description": "The presentation of the headline",
                "attribute_definitions": [{
                    "id": "cmsItem",
                    "name": "Headline Content",
                    "type": "cms_record",
                    "required": true,
                    "editor_definition": {
                        "content_type": "news"
                    }
                }]
            },
            {
                "id": "calltoaction",
                "name": "Call to Action Button",
                "description": "Label and target for the call to cation button",
                "attribute_definitions": [{
                        "id": "ctaTitle",
                        "name": "Button Title",
                        "type": "string",
                        "required": true
                    },
                    {
                        "id": "ctaLink",
                        "name": "Button Link",
                        "type": "url",
                        "required": true
                    }
                ]
            }
        ],
        "region_definitions": []
    }
    cmsheadlinebanner.js
    'use strict';
    
    var Template = require('dw/util/Template');
    var HashMap = require('dw/util/HashMap');
    var ImageTransformation = require('~/cartridge/experience/utilities/ImageTransformation.js');
    
    /**
     * Render logic for the assets.headlinebanner.
     */
    module.exports.render = function(context) {
        var model = new HashMap();
        var content = context.content;
    
        if (content.cmsItem) {
            model.bannerTitle = content.cmsItem.attributes.title;
            model.bannerCopy = content.cmsItem.attributes.body;
    
            if (content.cmsItem.attributes.bannerImage) {
                model.bannerImg = {
                    src: {
                        mobile: ImageTransformation.url(content.cmsItem.attributes.bannerImage, { device: 'mobile' }),
                        desktop: ImageTransformation.url(content.cmsItem.attributes.bannerImage, { device: 'desktop' })
                    },
                    alt: content.cmsItem.attributes.excerpt
                };
            }
        }
    
        model.callToAction = {
            title: content.ctaTitle,
            link: content.ctaLink
        }
    
        return new Template('experience/components/assets/headlinebanner').render(model).text;
    };

Use Salesforce CMS Content to Configure a Component Attribute 

Configure the component attribute by selecting the Salesforce CMS content and then configuring the attributes for that content.

When you select CMS content for a component attribute, the CMS content is copied into the page. If the original content in the CMS is updated, the changes don't automatically appear in the component. You must reconfigure the component attribute and select the content again to get the updated content.

The default locale for the Page Designer page is not necessarily the same as the default locale for the CMS content.

  1. Add the component that uses Salesforce CMS content to the page.

    In this example, the CMS Headline Banner component type uses CMS content to configure the Headline Content attribute. CMS headline banner component

  2. In the attribute editor window, click Select CMS Content.

    Select CMS content

    A list of the CMS content available to configure the attribute is displayed.

  3. Choose the CMS content you want to use and click Select.

    Set properties for CMS Headline Banner component

    The CMS content is copied to the page. Any attributes for the CMS content that require configuration are presented in the editing pane. The CMS content for the Headline Banner in the example requires that you enter a Title, Body, and Banner Image.

  4. Configure at least the required attributes for the component click Save or Save & Close.

Develop a Custom Attribute Editor 

You can create a custom attribute editor for configuring component attributes. Merchants use the custom attribute editor when setting a value for the component attribute in the Business Manager visual editor.

Why Implement a Custom Attribute Editor? 

When you create a component type for Page Designer, you specify attributes that the merchant can set. For example, you could create a headline banner component type with an attribute named Image. When merchants use the headline banner component type on a page, they must configure the Image attribute to select the image to display. The type value that you assign to the attribute in the component’s meta definition file determines the UI control (for example, a check box or file picker) that the merchant uses to configure the attribute.

Page Designer provides some preconfigured options for type. For example, if you assign the type file to an attribute, the Page Designer Visual Editor presents the merchant with a file picker UI control for selecting which file to use. If you assign the type boolean, the merchant is presented with a checkbox to configure the attribute. The type enum presents the merchant with a list to select a value from.

If none of the preconfigured Page Designer UI controls are appropriate for your situation, you can create your own custom attribute editor. For example, you might want a custom attribute editor to let merchants select store locations on a map or choose a color for the text or background. Or, you want to let merchants place a Favorite icon on a product tile or select which Shop Now button to use.

A custom attribute editor doesn’t conflict with the platform code for Page Designer, and you can create more than one custom attribute editor for a single component type.

How Does a Custom Attribute Editor Work? 

A custom attribute editor uses a meta definition file, script, and JavaScript and CSS resources to implement an editor that operates within a self-contained environment on the client side. The merchant uses the custom attribute editor in the Page Designer visual editor to configure a value for the attribute.

In the meta definition file for the component type, define the attribute's type as custom. If information is passed to the custom attribute editor, include that information in an editor_definition element for the attribute. In the meta definition file for the custom attribute editor, list the CSS and JavaScript resources that the custom attribute editor requires. You can optionally add server-side logic or resources to initialize the custom attribute editor in the script file for the custom attribute editor. You include the meta definition file and script file for the custom attribute editor, along with the JavaScript and CSS resources, in a custom cartridge assigned to the Business Manager site.

On the client side, each custom attribute editor is wrapped in a host component that contains an HTML <iframe> element. The iframe encapsulates the code and style of the editor and represents a self-contained sandbox environment in which the editor runs so that different custom attribute editors on the same page don't interfere with each other.

The host and the custom attribute editor in the iframe communicate with each other on a dedicated communication channel. Page Designer adds some management code to the iframe that includes a subscribe method and an emit method that send predefined events with serializable payloads back and forth between the host and the editor.

For information about the messaging channel implemented for Page Designer custom attribute editors, see Channel Messaging.

Custom Attribute in the Component Meta Definition File 

In the meta definition file for any standard component type, set the type of the attribute for which you want to use a custom attribute editor to custom and include an editor_definition element for the attribute. The editor_definition includes the custom attribute editor ID and optionally any configuration information that you want to pass to the custom attribute editor.

This example meta definition file for a banner includes three standard attributes: image of type file, alt of type string, and size of type enum. It also includes a custom attribute named Magic Unicorns, which is assigned to the custom attribute editor with ID com.sfcc.magical. The options for unicorns, for example, Auris and Chant, are listed in the configuration element in the editor_definition and passed to the custom attribute editor.

To view schema files that describe the JSON formatting for the meta definition file, see Page Designer JSON Schemas.

banner.json
{
  "name": "Banner",
  "description": "A banner.",
  "group": "content",
  "attribute_definition_groups": [
    {
      "id": "image",
      "name": "Banner Image Configuration",
      "description": "You can define the image file, size and alt text for the banner image.",
      "attribute_definitions": [
        {
          "id": "magic",
          "name": "Magic Unicorns",
          "type": "custom",
          "required": true,
          "editor_definition": {
            "type": "com.sfcc.magical",
            "configuration": {
              "options": {
                "config": [
                  "Auris",
                  "Chant",
                  "Elmas",
                  "Majesty",
                  "Moriba",
                  "Mystery",
                  "Mystic",
                  "Snowflake",
                  "Solstice",
                  "Sunshine"
                ]
              }
            }
          }
        },
{
          "id": "image",
          "name": "Banner Image",
          "description": "The image shown by the banner.",
          "type": "file",
          "required": true
        },
        {
          "id": "alt",
          "name": "Banner Image Alt Text",
          "description": "The image alt text shown by the banner.",
          "type": "string",
          "required": false
        },
        {
          "id": "size",
          "name": "Size",
          "description": "The banner size.",
          "type": "enum",
          "values": [
            "small",
            "medium",
            "large"
          ],
          "required": true,
          "default_value": "medium"
        }
      ],
      "region_definitions": []
    }
  ]
}

Custom Attribute Editor Meta Definition File 

The meta definition file for a custom attribute editor references the JavaScript and CSS resources that the custom attribute editor requires.

Put the meta definition file in this directory of the cartridge or any subdirectory under this directory:

  • {your_bm_ cartridge}/cartridge/experience/editors

The cartridge that contains the custom attribute editor meta definition file, script file, and client-side code must be added to the cartridge path for the Business Manager site.

Important

Use only alphanumeric or underscore characters for the file name. If you put the meta definition file in a subdirectory of /experience/editors, the subdirectory name must also use only alphanumeric or underscore characters.

To view the schema file that describes the JSON formatting for the meta definition file, see Page Designer JSON Schemas.

This example meta definition file describes a custom attribute editor that lets the user select a unicorn. You can use fully qualified URLs or relative URLs in the meta definition file. Page Designer resolves relative URLs to point to the/static/default directory of the cartridge. For example, this meta definition file uses relative URLs to reference the JavaScript file magical_editor.js and the CSS file magical.css, which are in the /static/default/experience/editors/com/sfcc directory of the cartridge.

magical.json
{
  "name": "Magical Custom Editor",
  "description": "A new magical editor with flying unicorns!",
  "resources": {
    "scripts": [
      "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js",
      "/experience/editors/com/sfcc/magical_editor.js"
    ],
    "styles": [
      "https://cdnjs.cloudflare.com/ajax/libs/design-system/2.8.3/styles/salesforce-lightning-design-system.min.css",
      "/experience/editors/com/sfcc/magical.css"
    ]
  }
}

Custom Attribute Editor Script File 

Each custom attribute editor requires a script file. The script file has the same name as the corresponding meta definition file but with a .js extension. For example, if the meta definition file for the control is magical.json, name the script magical.js.

Use only alphanumeric or underscore characters for the file name. Put the script file in the same location as its corresponding meta definition file. For example, for a cusotm attribute editor named magical, you could include the meta definition file and the script file in the following location:

my_bm_cartridge/cartridge/experience/editors/com/sfcc/magical.json

my_bm_cartridge/cartridge/experience/editors/com/sfcc/magical.js

The cartridge that contains the custom attribute editor meta definition file, script file, and client-side code must be added to the cartridge path for the Business Manager site.

Important

In the script file, you can optionally implement the init function to initialize the custom attribute editor with server-side logic or resources.

Every custom attribute editor requires a script file. If you don't use the script file to implement the init function, you must include an empty script file in the cartridge.

Note

This example script file adds options for the custom attribute editor not included in the editor_definition of the component's meta definition file. It also includes localization information and a reference to CSS resources that are required if the list of unicorns includes more than 10 items. The editor object passed to the init function is of type dw.experience.CustomEditor. It provides access to:

  • configuration—Map that contains entries passed to the client-side scripts, populated with the values provided by the editor_definition of the attribute in the component's meta definition file. To extend the map, use only serializable entries, primitive types (number string, boolean), dw.util.HashMap, or JavaScript arrays. Native JavaScript objects (in curly braces) aren’t supported nor are other complex B2C Commerce script objects, such as dw.catalog.Product.
  • dw.experience.CustomEditorResources—Collection of JavaScript and CSS URLs that the client-side iframe uses to display the custom attribute editor.
magical.js
'use strict';

var Resource = require('dw/web/Resource');
var HashMap = require('dw/util/HashMap');

module.exports.init = function (editor) {
  // add some more options programmatically
  editor.configuration.options.put('init', [
    'Wynstar',
    'Jasper',
    'Joshi',
    'Rainbow',
    'Blythe',
    'Mika',
    'Nightwind',
    'Cadillac',
    'Julius',
    'Calimerio'
  ]);

  // add some localizations
  var localization = {
    placeholder: 'Select your favorite unicorn',
    description: 'Unicorns are magical creatures you want for every component. Select the one of your choice now!',
    group1: 'Unicorns from JSON Config',
    group2: 'Unicorns from init()',
    group3: 'Unicorns from OCAPI request'
  };
  editor.configuration.put('localization', Object.keys(localization).reduce(function (acc, key) {
    acc.put(key, Resource.msg(key, 'experience.editors.com.sfcc.magical', localization[key]));
    return acc;
  }, new HashMap()));

  // add some resources only required for a lot of unicorns
  if ((editor.configuration.options.config.length + editor.configuration.options.init.length) > 10) {
     editor.resources.styles.push("/experience/editors/com/sfcc/magical-extreme.css");
  }
}

Client-Side JavaScript and CSS for a Custom Attribute Editor 

Put the client-side JavaScript files and CSS resources required to run the custom attribute editor in the static/default directory of the custom cartridge at a location that corresponds to the location of the meta definition file and script file for the editor.

For example, let's say the meta definition file and script file for the custom attribute editor are in the following location:

my_bm_cartridge/cartridge/experience/editors/com/sfcc/magical.json

my_bm_cartridge/cartridge/experience/editors/com/sfcc/magical.js

Put the JavaScript and CSS files here:

my_bm_cartridge/cartridge/static/default/experience/editors/com/sfcc

The cartridge that contains the custom attribute editor meta definition file, script file, and client-side code must be added to the cartridge path for the Business Manager site.

Important

This example of a client-side JavaScript file uses a <select> element to display data and interact with the user. The example displays two sets of unicorn types inside of <optgroup> elements. The two different types correspond to the two different sources from which the unicorns were passed to the editor.

  • group1—Unicorn types entered into the configuration element of the editor_definition for the attribute in the component's meta definition file.
  • group2—Unicorn types passed to the custom attribute editor from the init function of the editor's script file.

In this example, the custom attribute editor subscribes to the event sfcc:ready. As soon as this event is emitted by the host, the editor initializes its DOM (Document Object Model) using configuration and localization information from the init function of the server-side script file and assigning the unicorns to their appropriate <optgroup>. When the user changes the value of the <select> element, the editor sends the sfcc:value event to inform the host.

magical_editor.js
(() => {
  subscribe('sfcc:ready', async ({ value, config, isDisabled, isRequired, dataLocale, displayLocale }) => {
    console.log('sfcc:ready', dataLocale, displayLocale, value, config);

    const selectedValue = typeof value === 'object' && value !== null && typeof value.value === 'string' ? value.value : null;
    const { options = {}, localization = {} } = config;
    let isValid = true;

    // Append basic DOM
    const template = obtainTemplate(localization);
    const clone = document.importNode(template.content, true);
    document.body.appendChild(clone);

    // Set props
    const selectEl = document.querySelector('select');
    selectEl.required = isRequired;
    selectEl.disabled = isDisabled;

    // Set <options> from JSON config
    const optgroupEls = selectEl.querySelectorAll('optgroup');
    setOptions(options.config || [], optgroupEls[0], selectedValue);

    // Set <options> from init()
    setOptions(options.init || [], optgroupEls[1], selectedValue);

    // Apply change listener
    selectEl.addEventListener('change', event => {
      const val = event.target.value;
      emit({
        type: 'sfcc:value',
        payload: val ? { value: val } : null
      });
    });
  });

  function obtainTemplate({ placeholder, description, group1, group2 }) {
    const template = document.createElement('template');
    template.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
  <div class="slds-select_container" title="${description}">
    <select class="slds-select">
      <option value="">-- ${placeholder} --</option>
      <optgroup label="${group1}"></optgroup>
      <optgroup label="${group2}"></optgroup>
    </select>
  </div>
</div>`;
    return template;
  }

  function setOptions(options, optgroupEl, selectedValue) {
    options.forEach(option => {
      const optionEl = document.createElement('option');
      optionEl.text = option;
      optionEl.value = option;
      optionEl.selected = option === selectedValue;

      optgroupEl.appendChild(optionEl);
    });
  }
})();

Custom Attribute Editor Events 

Each custom attribute editor is wrapped in a host component that contains an HTML iframe element. The iframe encapsulates the code and styling of the editor and represents a self-contained sandbox environment in which the editor runs so that different custom attribute editors on the same page don't interfere with each other. The host and the custom attribute editor in the iframe communicate by passing events back and forth on a special messaging channel.

For information about the messaging channel implemented for Page Designer custom attribute editors, see Channel Messaging. For information about the messaging API used to send and receive the events, see Messaging API. Page Designer adds some management code to the iframe that includes a subscribe method and an emit method that send predefined events with serializable payloads back and forth between the host and the editor.

subscribe() 

Subscribes to events sent from the host to the editor. You can subscribe for a certain event type and when that event is sent from the host, a callback is invoked that uses a payload and optional context.

subscribe:Interface
type SandboxSubscribe = (type: string, callback: (payload?: any, context?: SandboxContext) => void, source?: any) => () => void;

interface SandboxContext {
  transfer?: Transferable[]; // @see https://developer.mozilla.org/en-US/docs/Web/API/Transferable
  source?: any; // Event sourcing should only be required in rare cases
}
subscribe Example
subscribe('sfcc:ready', (payload, context) => {
  // ...
});

emit() 

Sends events from the editor to the host.

emit:Interface
type SandboxEmit = (message: SandboxMessage, callback?: (payload: any) => void) => void;

interface SandboxMessage {
  type: string;
  payload?: any;
  source?: any;
  transfer?: Transferable | Transferable[];
emit Example
emit({
  type: 'sfcc:breakout',
  payload: {
    id: 'myBreakoutId',
    title: '
  }
}, ({ type, value }) => { // ... });
Table 1. Events Emitted by the Host
Event Meaning Payload
sfcc:ready The custom attribute editor is initialized. All the scripts and styles have been loaded into the editor's environment. When the host emits this event, it doesn't necessarily mean that all asynchronously loaded assets and code have finished loading. For some components, you might need to manually listen to browser events, such as load or DOMContentLoaded, to get more information. The sfcc:ready event includes information required to display the editor, such as initial value, configuration data, data locale, display locale, and initial validity.
{
value: { OBJECT_OF_ARBITRARY_STRUCTURE };
config: { OBJECT_OF_ARBITRARY_STRUCTURE };
isRequired: boolean;
isDisabled: boolean;
isValid: boolean;
dataLocale: string;
displayLocale: string;
}
sfcc:value The value of the attribute. Not sent on the initial load. Sent only when the value changes because of external modifications. { OBJECT_OF_ARBITRARY_STRUCTURE }
sfcc:required Indicates whether this attribute is required. Not sent on the initial load. Sent only when the required status changes after the initialization ready phase. The custom attribute editor might use this information to display certain styling or indicators in the editor. boolean
sfcc:disabled Indicates whether this attribute is disabled. Not sent on the initial load. Sent only when the disabled status changes after the initialization ready phase. The custom attribute editor might use this information to render elements differently or display certain styling or indicators in the editor. boolean
sfcc:valid Indicates whether the value of the attribute is valid. Not sent on the initial load. Sent only when the validity status changes after the initialization ready phase. boolean
EventMeaningPayload
sfcc:valueThe value of the attribute. Sent when the value changes inside the editor. Page Designer requires that the value be a plain JavaScript object.(OBJECT_OF_ARBITRARY_STRUCTURE)
sfcc:validIndicates whether the value of the attribute is valid. Can include an error message.{ valid: boolean, message: string }
sfcc:interactedIndicates that the user has interacted with the custom attribute editor. The editor is implicitly marked as interacted when it’s blurred, for example, when the editor's contentWindow loses focus. Page Designer supports an interacted (or touched) state for form elements. This state marks a field that a user already interacted with, for example, by tabbing through it. Being able to mark a field as touched allows for error messages in forms to be hidden initially and only display for fields with which a user has interacted.void

Create a Breakout Custom Attribute Editor That Displays in a Modal Window 

Create a custom attribute editor sized to fit your content with the breakout custom attribute feature.

What Is a Breakout Editor? 

The right pane of the Page Designer visual editor provides only limited space for merchants to configure component attributes. To provide more space, you can create a trigger editor to display a breakout editor inside a separate modal window.

For example, you want the merchant to select which unicorn image to use on the storefront, but the right pane doesn't have enough space to display all the available unicorn images. So you create a trigger editor that appears in the right pane and includes a Select button.

Editor for selecting a unicorn

When the merchant clicks Select, the breakout editor opens in a modal window where the merchant can see all the available images.

Figure showing images of unicorns, none selected.

The merchant selects an image, and clicks Apply.

List of unicorn images, one selected.

The modal closes, and the selected image appears in the right pane next to the Select button.

Editor for selecting a unicorn, showing the new selection.

Implementing a breakout editor involves these high-level steps:

  1. Create server-side meta definition and script files for the trigger editor and the breakout editor.
  2. In the script file for the trigger editor, define the breakout editor as a dependency.
  3. Develop client-side UI code that implements the trigger and breakout editors, opens and closes the breakout modal window, and passes a value from the breakout editor to the trigger editor to Page Designer when the merchant clicks Apply.

Trigger Editor Meta Definition and Script Files 

To create a trigger custom attribute editor, define the attribute as type custom in the component meta definition file. Then create a meta definition JSON file and a script file for the trigger editor. In the script file, instantiate the breakout editor using the PageMgr.getCustomEditor method. This method takes the ID of the breakout editor and an optional configuration object. Then add the breakout editor as a dependency.

This trigger editor script file example specifies a breakout editor with ID com.sfcc.magicalBreakout. The script creates a breakoutEditorConfig configuration object that contains the localization information defined for the trigger editor. It also includes the options defined in the attribute_definitions section of the component meta definition file for the trigger editor, obtained from editor.configuration.options.config.

The localization information and options in the breakoutEditorConfig object are passed from the trigger editor to the breakout editor using the PageMgr.getCustomEditor method, which instantiates the breakout editor with the given ID. The script then adds the breakout editor as a dependency using editor.dependencies.put. The ID included in editor.dependencies, in this case magical_breakout, is used in the client-side code for the trigger and breakout editors to access the breakout editor.

magical.js
'use strict';

var HashMap = require('dw/util/HashMap');
var Resource = require('dw/web/Resource');
var PageMgr = require('dw/experience/PageMgr');

module.exports.init = function(editor) {
  // Default values for L10N properties
  var l10nDefaults = {
    buttonBreakout: 'Select',
    titleBreakout: 'Breakout Content Title',
    placeholder: 'Select your favorite unicorn',
    description: 'Unicorns are magical creatures you want for every component. Select the one of your choice now!',
    group1: 'Unicorns from JSON Config',
    group2: 'Unicorns from init()'
  };

  // Add some localizations
  var localization = Object.keys(l10nDefaults).reduce(function (acc, key) {
    acc.put(key, Resource.msg(key, 'experience.editors.com.sfcc.magical', l10nDefaults[key]));
    return acc;
  }, new HashMap());
  editor.configuration.put('localization', localization);

  // Pass through property `options.config` from the `attribute_definition` to be used in a breakout editor
  var options = new HashMap();
  options.put('config', editor.configuration.options.config);

  // Create a configuration for a custom editor to be displayed in a modal breakout dialog (breakout editor)
  var breakoutEditorConfig = new HashMap();
  breakoutEditorConfig.put('localization', localization);
  breakoutEditorConfig.put('options', options);

  // Add a dependency to the configured breakout editor
  var breakoutEditor = PageMgr.getCustomEditor('com.sfcc.magicalBreakout', breakoutEditorConfig);
  editor.dependencies.put('magical_breakout', breakoutEditor);
};

Breakout Editor Meta Definition and Script Files 

The breakout custom attribute editor requires its own JSON meta definition file and script file.

The meta definition file references the JavaScript and CSS resources that the editor requires, as in this example.

magicalBreakout.json
{
  "name": "Magical Custom Editor Breakout",
  "description": "An editor with unicorn images",
  "resources": {
    "scripts": [
      "/experience/editors/com/sfcc/magical_breakout.js"
    ],
    "styles": [
      "https://cdnjs.cloudflare.com/ajax/libs/design-system/2.9.4/styles/salesforce-lightning-design-system.min.css",
      "/experience/editors/com/sfcc/magical.css"
    ]
  }
}

The script file implements the init function to initialize the custom attribute editor with server-side logic or resources. This example adds options for the breakout editor not included in the editor_definition of the component's meta definition file. It also includes a reference to CSS resources that are required if the list of unicorns includes more than 20 items.

magicalBreakout.js
'use strict';

var URLUtils = require('dw/web/URLUtils');

module.exports.init = function(editor) {
 var optionsConfig = editor.configuration.options.config;
 var optionsInit = [
  'Blythe',
  'Cadillac',
  'Calimerio',
  'Jasper',
  'Joshi',
  'Julius',
  'Mika',
  'Nightwind',
  'Rainbow',
  'Wynstar'
 ];

 // Add more unicorns programmatically
 editor.configuration.options.put('init', optionsInit);

 // Provide `baseUrl` to the static assets/content
 editor.configuration.put('baseUrl', URLUtils.staticURL('/experience/editors/com/sfcc/').https().toString());

 // Conditionally add a resource which is only required in case of a lot of unicorns
 if ((optionsConfig.length + optionsInit.length) > 20) {
  editor.resources.styles.push('/experience/editors/com/sfcc/magical_extreme.css');
 }
};

Open and Close a Breakout Editor 

To open the modal window for the breakout editor, we recommend using a callback on event emission approach, which manages the asynchronous breakout editor process. If you have multiple breakout editors, the callback on event emission approach makes it easier to keep track of open breakout editors because a callback is related to only one breakout process.

This example opens the breakout editor from the trigger editor, waits for the user to close the breakout editor, and then gets the selected value.

// Code in the client-side JavaScript file for the trigger editor
(() => {
  // 1)
  subscribe("sfcc:ready", () => {
    // Once the editor is ready, a `sfcc:breakout` event is sent. Usually
    // this happens when a user clicks on a certain <button> or interacts with the trigger editor in some way
    const openButtonEl = document.querySelector("#myOpenBtn");
    openButtonEl.addEventListener("click", handleBreakoutOpen);
  });

  function handleBreakoutOpen() {
    // 2)
    // The user interacted with the <button> and the triggering editor requests that Page Designer
    // open a breakout on its behalf using an editor ID. This ID refers
    // to the key under which an editor has been put into the "dependencies" key-
    // value store of the triggering editor's configuration. A callback is provided
    // along with the event emission to be invoked once the breakout editor gets
    // closed.
    emit(
      {
        type: "sfcc:breakout",
        payload: {
          id: "myCustomEditorId",
          title: "The title to be displayed in the modal",
        },
      },
      handleBreakoutClose,
    );
  }

  function handleBreakoutClose({ type, value }) {
    // 5)
    // When the breakout is closed (either programmatically, or via the
    // built-in action <button>s), the callback is invoked, containing
    // information about the "type" of action that caused the invocation.
    // A breakout can be closed with the intent to either "Cancel" or "Apply"
    // an editing process. The intent can be derived from the "type" information
    // that comes as part of the callback reponse (either `sfcc:breakoutCancel`
    // or `sfcc:breakoutApply`). In case of an "Apply" intent will also include
    // a "value" available as part of the response, reflecting the value that has
    // last been set via `sfcc:value` in the breakout editor.

    // Now the "value" can be passed back to Page Designer
    if (type === "sfcc:breakoutApply") {
      handleBreakoutApply(value);
    } else {
      handleBreakoutCancel();
    }
  }

  function handleBreakoutCancel() {
    // ...
  }

  function handleBreakoutApply(value) {
    // ...

    // Emit value update to Page Designer host application
    emit({
      type: "sfcc:value",
      payload: value,
    });
  }
})();
// Code in the client-side JavaScript file for the breakout editor
(() => {
  // 3)
  subscribe("sfcc:ready", () => {
    // Once the breakout editor is ready, the custom code is able to select or create
    // a value. Any meaningful change to a value/selection needs to be reflected
    // back into the host container via a `sfcc:value` event.
    const selectEl = document.querySelector("#mySelect");
    selectEl.addEventListener("change", handleSelect);
  });

  function handleSelect({ target }) {
    // 4)
    // The value changed and the breakout editor's host is informed about the
    // value update via a `sfcc:value` event.
    const selectedValue = target.value;
    emit({
      type: "sfcc:value",
      payload: selectedValue ? { value: selectedValue } : null,
    });
  }
})();

Another way to open and close the breakout modal is to use only events. However, this approach is more difficult because it requires that you correctly implement a cascade of subscribers and emitters based on the messaging API. This example illustrates a trigger editor that manages multiple parallel breakdout editors.

If you leave the ID portion of an event payload blank, Page Designer closes the first open breakout editor related to the requesting trigger editor. `

Note

// Code in the client-side JavaScript file for the trigger editor
(() => {
  // 1)
  subscribe("sfcc:ready", () => {
    // Once the editor is ready, a `sfcc:breakout` event can be sent. Usually
    // this happens when a user clicks on a certain <button> or interacts with the trigger editor in some way
    const openButtonEl = document.querySelector("#myOpenBtn");
    openButtonEl.addEventListener("click", handleBreakoutOpen);
  });

  // 3b)
  subscribe("sfcc:breakout", ({ id }) => {
    // Once the Page Designer host application opens a breakout on the trigger editor's behalf,
    // it informs the trigger about its newly created breakout via a `sfcc:breakout`
    // event. The important property of the payload is the "id" which can be stored
    // to be used later to explicitly close a certain breakout component. There is
    // no hard requirement listening to this event in case the custom code inside
    // the triggering editor doesn't have to explicitly close a certain breakout
    // programmatically. Usually a breakout will be closed by the breakout itself,
    // either via the custom code inside the breakout CAE, or via user interaction
    // with a built-in <button> in a breakout's footer section.
  });

  // 6a)
  subscribe("sfcc:breakoutCancel", ({ id }) => {
    // ...
  });

  // 6b)
  subscribe("sfcc:breakoutApply", ({ id, value }) => {
    // ...

    // Emit value update to Page Designer
    emit({
      type: "sfcc:value",
      payload: value,
    });
  });

  function handleBreakoutOpen() {
    // 2)
    // The user interacted with the <button> and the triggering CAE requests that Page Designer
    // open a breakout on its behalf using a certain editor ID. This ID refers
    // to the key under which an editor has been put into the "dependencies" key-
    // value store of the triggering editor's configuration. This time no callback
    // is provided along with the event emission.
    emit({
      type: "sfcc:breakout",
      payload: {
        id: "myCustomEditorId",
        title: "The title to be displayed in the modal",
      },
    });
  }
})();
// Code in the client-side JavaScript file for the breakout editor
(() => {
  // 3a)
  subscribe("sfcc:ready", ({ breakout }) => {
    // Along with the `sfcc:ready` event, some meta information about the breakout is
    // passed to the breakout editor. The important property in the "breakout" data
    // is the "id" which can be stored to be used later to explicitly close a
    // certain breakout component.

    // Once the breakout editor is ready, the custom code is able to select or create
    // a value. Any meaningful change to a value or selection must be reflected
    // back into the host container via a `sfcc:value` event.

    // 3)
    // Apply event handlers to elements inside the component's DOM.
    // It's not required to have action buttons inside the editor itself as the
    // surrounding breakout element's footer section already contains <button>
    // elements for both "Cancel" and "Apply" actions.
    const cancelButtonEl = document.querySelector("#myCancelBtn");
    const applyButtonEl = document.querySelector("#myApplyBtn");
    const selectEl = document.querySelector("#mySelect");
    const { id } = breakout;
    cancelButtonEl.addEventListener("click", () => handleBreakoutCancel(id));
    applyButtonEl.addEventListener("click", () => handleBreakoutApply(id));
    selectEl.addEventListener("change", handleSelect);
  });

  function handleSelect({ target }) {
    // 4)
    // The value changed and the breakout editor's host gets informed about the
    // value update via a `sfcc:value` event.
    const selectedValue = target.value;
    emit({
      type: "sfcc:value",
      payload: selectedValue ? { value: selectedValue } : null,
    });
  }

  function handleBreakoutCancel(id) {
    // 5a)
    // Explicitly request that the Page Designer host application close a certain breakout with
    // the intent to cancel the editing process.
    emit({
      type: "sfcc:breakoutCancel",
      payload: { id },
    });
  }

  function handleBreakoutApply(id) {
    // 5b)
    // Explicitly request that the Page Designer host application close a certain breakout with
    // the intent to apply the current value/selection of the editing process.
    emit({
      type: "sfcc:breakoutApply",
      payload: { id },
    });
  }
})();

Enable and Disable the Breakout Editor Apply Button 

You can emit an sfcc:valid event from the breakout editor to indicate whether the value that the user selected in the breakout editor is valid. If the value is valid, Page Designer enables the Apply button in the breakout modal window footer. If the value is invalid, Page Designer disables the Apply button and optionally displays an error message.

In this example, if the value is valid, an sfcc:valid event with payload true is emitted, and the Apply button is enabled. If the value is invalid, an sfcc:valid event is issued with a polymorphic payload that includes a valid key set to false and a message set to This value is invalid. The Apply button is disabled, and an error icon is displayed showing the error message This value is invalid in a tooltip.

// Code in the client-side JavaScript file for the breakout editor
(() => {
  // ...

  if (isValid) {
    emit({
      type: 'sfcc:valid',
      payload: {
        valid: false,
        message: 'This value is invalid.'
      }
    });
  } else {
    emit({
      type: 'sfcc:valid',
      payload: true
    });
  }

  // ...
})();

Trigger and Breakout Editor Client-Side UI Code 

The trigger editor and the breakout editor require client-side script files. You can incorporate any of the custom attribute editor events in the script files. The trigger editor file opens and closes the breakout editor and emits the sfcc:value event to Page Designer. The breakout editor file includes the code to implement the editor, and emits sfcc:valid and sfcc:value events to the trigger editor.

This example of a client-side script for a trigger editor, magical_trigger.js, uses callbacks and event emissions to open and close the breakout editor. When the merchant clicks Apply in the breakout editor, the handleBreakoutApply(value) function passes the value selected in the breakout editor to Page Designer using an sfcc:value event.

The client-side script example for the corresponding breakout editor, magical_breakout.js, includes the logic that displays the unicorns. The list of unicorns to display is retrieved from two different sources. The first source is the attribute_definitions in the component's meta definition file (options.config). Refer to Custom Attribute Editor Meta Definition File for an example of the component's meta definition file where these unicorn options are configured. The second source is the init method in the server-side script file (options.init).

The example uses function updateValidity(value) to check whether the currently selected value. When the merchant selects a valid value, function updateSelectedValue(value) emits an sfcc:value event to the dedicated host component for the breakout editor.

The code in these examples conforms to the ECMAScript 2015 specification. It isn't appropriate if you’re targeting legacy browsers such as Internet Explorer 11.

Note

static/default/experience/editors/com/sfcc/magical_trigger.js
(() => {
  let localization;
  let inputEl;
  let buttonEl;

  subscribe('sfcc:ready', async ({ value, config, isDisabled, isRequired, dataLocale, displayLocale }) => {
    console.log('magical-trigger::sfcc:ready', dataLocale, displayLocale, isDisabled, isRequired, value, config);

    // Extract `localization` data from `config`
    ({ localization = {} } = config);

    // Initialize the DOM
    const template = obtainTemplate();
    const clone = document.importNode(template.content, true);
    document.body.appendChild(clone);

    // Obtain DOM elements and apply event handlers
    inputEl = document.querySelector('input');
    buttonEl = document.querySelector('button');
    buttonEl.addEventListener('click', handleBreakoutOpen);

    // Update <input> value
    inputEl.value = obtainDisplayValue(value);
  });

  function obtainTemplate() {
    const { placeholder, buttonBreakout } = localization;
    const template = document.createElement('template');
    template.innerHTML = `
<div class="slds-grid slds-grid_vertical-align-center">
  <div class="slds-col slds-grow">
    <input type="text" disabled class="slds-input" placeholder="${placeholder}">
  </div>
  <div class="slds-col slds-grow-none">
    <button type="button" class="slds-button slds-button_neutral">${buttonBreakout}</button>
  </div>
</div>`;
    return template;
  }

  function obtainDisplayValue(value) {
    return typeof value === 'object' && value != null && typeof value.value === 'string' ? value.value : null;
  }

  function handleBreakoutOpen() {
    const { titleBreakout } = localization;

    emit({
      type: 'sfcc:breakout',
      payload: {
        id: 'magical_breakout',
        title: titleBreakout
      }
    }, handleBreakoutClose);
  }

  function handleBreakoutClose({ type, value }) {
    if (type === 'sfcc:breakoutApply') {
      handleBreakoutApply(value);
    } else {
      handleBreakoutCancel();
    }
  }

  function handleBreakoutCancel() {
    console.log('magical-trigger::sfcc:breakoutCancel');

    // Grab focus
    buttonEl && buttonEl.focus();
  }

  function handleBreakoutApply(value) {
    console.log('magical-trigger::sfcc:breakoutApply');

    // Update <input> value
    inputEl.value = obtainDisplayValue(value);

    // Emit value update to Page Designer
    emit({
      type: 'sfcc:value',
      payload: value
    });

    // Grab focus
    buttonEl && buttonEl.focus();
  }
})();
static/default/experience/editors/com/sfcc/magical_breakout.js
(() => {
  let baseUrl;
  let localization;
  let options;
  let gridEl;

  subscribe('sfcc:ready', async ({ value, config, isDisabled, isRequired, dataLocale, displayLocale }) => {
    console.log('magical-breakout::sfcc:ready', dataLocale, displayLocale, isDisabled, isRequired, value, config);

    // Extract essential data from `config`
    ({ baseUrl = './', options = {}, localization = {} } = config);

    // Initialize the DOM
    const template = obtainTemplate();
    const clone = document.importNode(template.content, true);
    document.body.appendChild(clone);

    // Update initial validity
    const selectedValue = obtainDisplayValue(value);
    updateValidity(selectedValue);

    // Init and append unicorn DOM
    const { group1, group2 } = localization;
    gridEl = document.querySelector('.slds-grid');
    options.config.forEach(appendItems(group1, selectedValue));
    options.init.forEach(appendItems(group2, selectedValue));

    // If applicable, focus the link element that represents the currently selected unicorn
    // --> 100ms delay due to an SLDS transition interference
    const selectedEl = document.querySelector(`a[data-value="${selectedValue}"]`);
    selectedEl && setTimeout(() => {
      selectedEl.scrollIntoView(false);
    }, 100);
  });

  function obtainTemplate() {
    const template = document.createElement('template');
    template.innerHTML = `
<div class="slds-grid slds-grid_vertical"></div>`.trim();
    return template;
  };

  function obtainItemHeadTemplate(title) {
    const template = document.createElement('template');
    template.innerHTML = `
<div class="slds-col slds-box">${title}</div>`.trim();
    return template;
  }

  function obtainItemTemplate(option) {
    // Choose a random unicorn image -> Generate a number between 1 and 20
    const unicornImageId  = Math.floor(Math.random() * 20) + 1;

    const template = document.createElement('template');
    template.innerHTML = `
<div class="slds-col">
  <a href="javascript:void(0);" tabindex="-1" title="${option}" class="slds-box slds-box_link slds-box_x-small slds-media" data-value="${option}">
    <div class="slds-media__figure slds-media__figure_fixed-width slds-align_absolute-center slds-m-left_xx-small">
      <span class="slds-avatar slds-avatar_large">
        <img src="${baseUrl}images/unicorn_${unicornImageId}.jpg" alt="${option}" loading="lazy">
      </span>
    </div>
    <div class="slds-media__body slds-border_left slds-p-around_small">
      <h2 class="slds-truncate slds-text-heading_small" title="Share the knowledge">${option}</h2>
      <p class="slds-m-top_small">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugiat minus molestias reprehenderit consequuntur sapiente. Modi veritatis totam accusantium numquam assumenda.</p>
    </div>
  </a>
</div>`.trim();
    return template;
  }

  function appendItems(title, selectedValue) {
    const itemHeadTemplate = obtainItemHeadTemplate(title);
    const itemHeadEl = document.importNode(itemHeadTemplate.content, true);
    gridEl.appendChild(itemHeadEl);

    return option => {
      // Init a media object inside a grid column to represent a unicorn
      const itemTemplate = obtainItemTemplate(option);
      const itemEl = document.importNode(itemTemplate.content, true);

      // Init the link inside the media object
      const isSelected = selectedValue === option;
      const linkEl = itemEl.querySelector('a');
      linkEl.addEventListener('click', handleSelect);
      isSelected && linkEl.classList.add('slds-is-selected');

      // Append the grid column to the DOM
      gridEl.appendChild(itemEl);
    };
  }

  function handleSelect({ currentTarget }) {
    const { value } = currentTarget.dataset;
    updateSelectedValue(value);
  }

  function updateValidity(value) {
    const isValid = typeof value !== 'undefined' && value != null;
    const { description } = localization;
    const payload = isValid ? isValid : { valid: isValid, message: description };

    emit({
      type: 'sfcc:valid',
      payload
    });

    return isValid;
  }

  function updateSelectedValue(value) {
    const oldSelectedEl = document.querySelector('a.slds-is-selected');
    oldSelectedEl && oldSelectedEl.classList.remove('slds-is-selected');

    const isValid = updateValidity(value);
    if (isValid) {
      const newSelectedEl = document.querySelector(`a[data-value="${value}"]`);
      newSelectedEl && newSelectedEl.classList.add('slds-is-selected');

      emit({
        type: 'sfcc:value',
        payload: {
          value
        }
      });
    }
  }

  function obtainDisplayValue(value) {
    return typeof value === 'object' && value != null && typeof value.value === 'string' ? value.value : null;
  }
})();

Use a Prebuilt Editor in a Custom Attribute Editor 

Create custom attribute editors faster and more efficiently with prebuilt attribute editors. You can use the prebuilt editors as building blocks when you create custom attribute editors.

This table lists the prebuilt editors that are available. We provide multiple IDs for each prebuilt editor so that you can use the style you prefer. All the prebuilt editors open in a separate breakout modal window.

Table 1. Prebuilt Editors
Prebuilt EditorPurposeIDReturn Value TypeReturn Value Example
Category PickerAllows merchant to select a categorysfcc:categoryPicker or sfcc\:category-pickerCategory ID
{
type: "sfcc:categoryPicker",
value: "1234567890"
}
Link BuilderBuilds a link to the element selected by the merchantsfcc:linkBuilder or sfcc:link-builderAbsolute URL or URL portions

Depending on the link type the user created using the Link Builder, the return value is either a single string or an array of strings.

A single string indicates that the value is an absolute URL that can be used as-is. An array of strings indicates that the URL must be assembled using the URLUtils.url method.

{
type: "sfcc:linkBuilder",
value: "https://salesforce.com"
}

or

{
type: "sfcc:linkBuilder",
value: ["Page-Show", "cid", "1234567890"]
}
Page PickerAllows merchant to select a pagesfcc:pagePicker or sfcc:page-pickerPage ID
{
type: "sfcc:pagePicker",
value: "1234567890"
}
Product PickerAllows merchant to select a productsfcc:productPicker or sfcc:product-pickerProduct ID
{
type: "sfcc:productPicker",
value: "1234567890"
}

The following examples show a custom attribute editor that uses the prebuilt editor for Category Picker. In the meta definition file for the component type, designate the attribute for the Category Picker as type custom.

selectionTile.json
{
	"name": "Category Selection Tile",
	"description": "A tile that lets the mercant select a category to display.",
	"group": "content",
	"attribute_definition_groups": [{
		"id": "category",
		"name": "Category",
		"description": "You can select the category.",
		"attribute_definitions": [{
			"id": "category",
			"name": "Category",
			"type": "custom",
			"required": true,
			"editor_definition": {
				"type": "com.sfcc.categoryPicker"
			}
		}],

On the server side, create a meta definition JSON file and a .js file for the custom attribute editor just as you would for any custom attribute editor.

categoryPicker.json
{
  "name": "Category Editor",
  "description": "A prebuilt editor that allows you to select a category.",
  "resources": {
    "scripts": [
      "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js",
      "/experience/editors/com/sfcc/categoryPicker_trigger.js"
    ],
    "styles": [
      "https://cdnjs.cloudflare.com/ajax/libs/design-system/2.8.3/styles/salesforce-lightning-design-system.min.css",
      "/experience/editors/com/sfcc/category.css"
    ]
  }
}
categoryPicker.js
'use strict';

var HashMap = require('dw/util/HashMap');
var Resource = require('dw/web/Resource');

module.exports.init = function (editor) {
    // Default values for L10N properties
    var l10nDefaults = {
        buttonBreakout: 'Select',
        titleBreakout: 'Category',
        placeholderInput: 'Select the category',
    };

    // Add some localizations
    var localization = Object.keys(l10nDefaults).reduce(function (acc, key) {
        acc.put(key, Resource.msg(key, 'experience.editors.sfcc.categoryPicker', l10nDefaults[key]));
        return acc;
    }, new HashMap());

    if (!editor.configuration) {
      editor.configuration = new HashMap();
    }
    editor.configuration.put('localization', localization);
};

On the client side, create a JavaScript file that runs the prebuilt editor, as in this example. Set the payload ID in the sfcc:breakout event to the ID of the prebuilt editor, in this case sfcc:categoryPicker.

categoryPicker_trigger.js
(() => {
  let localization;
  let inputEl;
  let buttonEl;/**
   * This listener subscribes to the creation/initialization event of the sandbox component. This event gets fired when a Page Designer user
   * first opens a component of a certain type in the component settings panel (and that component type contains the custom editor).
   * **Note:** This event won't fire when the user switches between components of the same type in the canvas. In that case the existing
   * attribute editor components will be reused which results in a value update event (`sfcc:value`), therefore no new initialization
   * happens.
   */
  subscribe('sfcc:ready', ({ value, config, isDisabled, isRequired, dataLocale, displayLocale }) => {
    console.log('category-trigger::sfcc:ready', { dataLocale, displayLocale, isDisabled, isRequired, value, config });// Extract data from `config`
    ({ localization = {} } = config);// Initialize the DOM
    const template = obtainTemplate();
    const clone = document.importNode(template.content, true);
    document.body.appendChild(clone);// Obtain DOM elements and apply event handlers
    inputEl = document.querySelector('input');
    buttonEl = document.querySelector('button');
    buttonEl.addEventListener('click', handleBreakoutOpen);// Update <input> value
    inputEl.value = obtainDisplayValue(value);
  });/**
   * This listener subscribes to external value updates. As stated above, this event also gets fired when the user switches between
   * different components of the same type in the canvas. As in that case already existing custom editor instances get reused, it
   * technically means that switching between components of the same type is just an external value update.
   */
  subscribe('sfcc:value', value => {
    console.log('category-trigger::sfcc:value', { value });// Update <input> value
    inputEl.value = obtainDisplayValue(value);
  });function obtainTemplate() {
    const { placeholderInput, buttonBreakout } = localization;
    const template = document.createElement('template');
    template.innerHTML = `
<div class="slds-grid slds-grid_vertical-align-start">
  <div class="slds-col slds-grow">
    <input type="text" disabled class="slds-input" placeholder="${placeholderInput}">
  </div>
  <div class="slds-col slds-grow-none slds-m-left_xx-small">
    <button type="button" class="slds-button slds-button_neutral">${buttonBreakout}</button>
  </div>
</div>`;
    return template;
  }function obtainDisplayValue(value) {
    return typeof value === 'object' && value != null && typeof value.value === 'string' ? value.value : null;
  }function handleBreakoutOpen() {
    const { titleBreakout } = localization;emit({
      type: 'sfcc:breakout',
      payload: {
        id: 'sfcc:categoryPicker',
        title: titleBreakout
      }
    }, handleBreakoutClose);
  }function handleBreakoutClose({type, value}) {
    if (type === 'sfcc:breakoutApply') {
      handleBreakoutApply(value);
    } else {
      handleBreakoutCancel();
    }
  }function handleBreakoutCancel() {
    // Grab focus
    buttonEl && buttonEl.focus();
  }function handleBreakoutApply(value) {
    // Update <input> value
    inputEl.value = obtainDisplayValue(value);// Emit value update to the PD host application
    emit({
      type: 'sfcc:value',
      payload: value
    });// Grab focus
    buttonEl && buttonEl.focus();
  }
})();

Search Configuration for Page Designer Pages 

Configure searching for Page Designer pages and attribute content.

Enable Search and Search Suggestions for Page Designer Pages 

Page Designer pages and components are content assets that can optionally be assigned to folders. In order to also make pages that aren’t assigned to folders searchable, you must extrend the ContentSearchModel to remove the folder filter. The same applies to content suggestions, so you want to apply the same adjustment to the SuggestModel as well.

For example, in the Storefront Reference Architecture (SFRA) searchHelpers.js file, we could add the following line to the setupContentSearch function so that searching can find content assets that aren’t in folders:

apiContentSearchModel.setFilteredByFolder(false);

The modified setupContentSearch function is as follows:

function setupContentSearch(params) {
    var ContentSearchModel = require('dw/content/ContentSearchModel');
    var ContentSearch = require('*/cartridge/models/search/contentSearch');
    var apiContentSearchModel = new ContentSearchModel();

    apiContentSearchModel.setRecursiveFolderSearch(true);
    apiContentSearchModel.setSearchPhrase(params.q);
    **apiContentSearchModel.setFilteredByFolder(false);**
    apiContentSearchModel.search();
    var contentSearchResult = apiContentSearchModel.getContent();
    var count = Number(apiContentSearchModel.getCount());
    var contentSearch = new ContentSearch(contentSearchResult, count, params.q, params.startingPage, null);

    return contentSearch;
}

In the Storefront Reference Architecture (SFRA) SearchServices.js file, we could set the setFilteredByFolder attribute of the SuggestModel to false so that content assets that aren’t in folders can be found using content suggestions:

suggestions.setFilteredByFolder(false);

The modified code is as follows:

if (searchTerms && searchTerms.length >= minChars) {
        suggestions = new SuggestModel();
        **suggestions.setFilteredByFolder(false);**
        suggestions.setSearchPhrase(searchTerms);
        suggestions.setMaxSuggestions(maxSuggestions);
        categorySuggestions = new CategorySuggestions(suggestions, maxSuggestions);
}

Enable Indexing for Attribute Content 

You can specify that component or page attributes are indexed and searchable. When attributes are searchable, users can find pages by searching for the attribute value. For example, users can search for the text configured for a rich text attribute.

Add searching to the definition of the attribute in the page or component meta definition file, as in the following example for a page attribute:

{
  "id": "heading",
  "name": "Banner Heading",
  "required": true,
  "type": "text",
  "searching": {
    "searchable": true,
    "refinable": true,
    "boost_factor": 2.0
    "sortable": true
  }
},

You can designate the following types as searchable. If the searchable value isn’t specified, the default is false.

  • string
  • text
  • markup
  • product
  • category
  • cms_record
  • custom
  • enum-of-string

The refinable value indicates whether a user can further refine search results based on the content of this attribute. You can designate all searchable attribute types as refinable except cms_record and custom.

The boost_factor value influences the placement of the page in the search results. You can specify any value from 0.01 to 100.00 for the boost factor. A boost factor of 1.00 is the default and doesn’t change the order of search results. Specify a boost factor less than 1.00 to indicate that text in the attribute is less relevant than other terms in the index. Specify a boost factor greater than 1.00 to increase the relevance of attribute text. For example, including a boost factor of 2.00 makes the field twice as relevant in relation to other fields in the index. We recommend that you use boost factors less than or equal to 5.00.

For page attributes, you can include the sorting property set to true to indicate that search results can be sorted using this attribute.

The sorting property is applicable only for page-level attributes. Don’t use the sorting property for component attributes.

Note

You can’t use the sorting property with attributes of type markup.

Note

For attributes of type custom, all elements of the attribute are considered text for purposes of indexing. For attributes of type cms_record, only attributes of the cms_record that are supported as searchable are added to the index. For example, if the cms_record includes an attribute of type image, the image isn’t indexed.

Use SEO Page Meta Tag Rules for Dynamic Pages 

When you render dynamic Product Detail or Product List pages, use Page Meta Tag Rules that merchants configured to create meta tags in the HTML markup. Include a decorator that creates the HTML <head> section of the page. When rendering the page, get the data from the Page Meta Tag Rules, and pass it to the decorator.

For example, if you’re working with SFRA pages and components, you can include the following code in the implementation of your page, after all regions and all components are rendered, but before the decorator.

pdict.CurrentPageMetaData = PageRenderHelper.getPageMetaData(pdict.page);

The template htmlHead.isml, which is included indirectly in the SFRA decorator templates, uses pdict.CurrentPageMetaData to create the appropriate HTML meta tags.

Pages and Components as Content Assets 

Pages and components are persisted in the database as content assets, but they aren't displayed in the content asset list in Business Manager and you can't edit them directly from Business Manager. Generally, processes like replication and indexing work the same for pages and components as they do for content assets. But pages and components are different from content assets in some ways.

Pages and Components and OCAPI SHOP and DATA API 

Page Designer doesn't support using the OCAPI SHOP and DATA resources to access pages and components as content assets.

Page and Component Content Asset Attributes 

Pages and components share the same attributes as content assets. But in some cases, the content asset attributes have special meaning or behavior when they’re applied to pages and components.

Content Asset AttributePagesComponentsLocalizable?Site Specific?
ididid  
typetypetype  
onlineonlineonline X
configvisibility definitionvisibility definition  
data content attributesX 
content linkscomponents per regioncomponents per region  
namenamenameX (For components, the name is technically localizable, but the localized name isn’t displayed in the visual editor.) 
descriptiondescription X 
page titleseo title X 
page descriptionseo description X 
page keywordsseo keywords X 
page urlseo url prefix X 
searchablesearchable  X
searchwordssearchwords X 
sitemap includedsitemap included  X
sitemap change frequencysitemap change frequency  X
sitemap prioritysitemap priority  X

Import and Export for Page Designer Pages and Components 

You can import pages and components as content assets using the standard library import and export functionality. The type attribute distinguishes pages and components in the library from other content assets.

Pages use the following format for the type attribute:

page.{page_type_id}

For example, this snippet from a library import file includes a page named fixedlayout identified as type page.fixedlayout.

<content content-id="finishedsamplepage">
   <display-name xml:lang="x-default">Finished Sample Page</display-name>
   <display-name xml:lang="en-US">Finished Sample Page</display-name>
   <type>page.fixedlayout</type>
   <config>{
     "visibility" : [ ]
   }</config>
   <online-flag>true</online-flag>
   <searchable-flag>false</searchable-flag>
   <page-attributes/>
   <content-links>
       <content-link content-id="headlinebanner_newarrivals-womens"
        type="page.fixedlayout.region1">
           <position>0.0</position>
       </content-link>
   </content-links>
   <sitemap-included-flag>false</sitemap-included-flag>
</content>

Components use the following format for the type attribute:

component.{component_type_id}

For example, this snippet from a library import file includes a component named producttile identified as type component.assets.producttile.

<content content-id="producttile_25696677">
        <display-name xml:lang="x-default">Product Tile 25696677</display-name>
        <type>component.assets.producttile</type>
        <config>{
          "visibility" : [ ]
        }</config>
        <data xml:lang="x-default">{
          "product" : "25696677"
        }</data>
        <data xml:lang="en-US">{
          "product" : "25696677"
        }</data>
        <online-flag>true</online-flag>
        <page-attributes/>
    </content>

Both pages and components must include a config attribute that describes visibility rules. Components must also include a data attribute that describes the component attributes set by the merchant. The config and data attributes must be defined as JSON snippets. Refer to the JSON schemas for details:

Page Designer JSON Schemas

In the following situations, the import is allowed to proceed, but might issue multiple warning messages:

  • You haven’t yet uploaded the cartridge that contains the page and component types.
  • The imported data is in conflict with the meta definitions for existing page and component types, for example, if an imported component has attributes that aren’t defined in the corresponding meta definition file.

Page Designer Caching 

It’s important to understand how page caching works with Page Designer pages and the cache times for pages, components, and custom attribute editors.

Page Caching 

Caching for Page Designer pages is similar to other pages, with some nuances.

Controller Pagecache Lifecycle 

The rendering or serialization of a page starts with a top-level request from the web adapter that invokes a custom controller, such as Page-Show, on the application server. For example, the Page.js controller from the SFRA Page Designer reference implementation uses the following snippet to check if the page is visible. Only pages with no visibility rules are pagecached.

server.get('Show', cache.applyDefaultCache, consentTracking.consent, function (_req_, res, next) {
   ...
   var page = PageMgr.getPage(req.querystring.cid);

   if (page != null && page.isVisible()) {
      if (page.hasVisibilityRules()) {
         res.cachePeriod = 0; // pages with visibility rules should not be cached
         res.cachePeriodUnit = 'hours';
      }

      res.page(page.ID, {}); // invokes PageMgr.renderPage()
   } else {
      ...
   }

   next();
}, pageMetaData.computedPageMetaData);

Deciding if you want to apply pagecaching in your controller, and how to do so, is effectively a case by case decision. Don’t create pagecache conditions in your code that are subject to the request context, thus not pagecacheable in a global manner.

Page Rendering Using Nested Remote Includes to Handle Visibility Rules 

Parallel to the first pagecache lifecycle constructed around the controller, there’s a second pagecache lifecycle constructed around the actual contents of the page that is initiated by two nested remote includes.

When PageMgr.renderPage() renders or PageMgr.serializePage() serializes a page:

  • The first-level remote include is the system controller __SYSTEM__Page-Include. This remote include determines a visibility fingerprint of the page and its components, which are visible for the current request context (based on schedules, customer groups, or other visibility settings). Due to this context evaluation, this remote include is never going to be pagecached - or in other words, any page rendering or serialization only hits this remote include one time. It then passes the visibility fingerprint to the second-level remote include.
  • The second-level remote include is the system controller __SYSTEM__Page-Render (rendering) or __SYSTEM__Page-Serialize (serialization), which is pagecachable based on the attached visibility fingerprint. This remote include invokes the custom scripts of the render function to render the page, or the serialize function to serialize the page. The response created is configured with pagecache settings through the previously mentioned scripts that are invoked for the page and all its processed visible components.

Each variation of the page showing a different set of visible components results in a separate pagecache entry because the visibility fingerprint calculation yields a different result.

The more a page varies (in terms of different components shown for different request contexts) the more pagecache fragmentation for this page occurs.

Note

If you check caching for the first-level remote include, __SYSTEM__Page-Include, all invocations appear uncached. The second-level remote include, __SYSTEM__Page-Render or __SYSTEM__Page-Serialize, maintains caching statistics for Page Designer pages but those caching statistics track caching for all Page Designer pages. If you have several Page Designer pages, such as a home page and a Product Detail page, you can't track caching separately per page.

Page and Component Pagecache Lifecycle 

The pagecache lifecycle of the page contents is induced through a separate request. This isn’t tied to the first pagecache lifecycle of the top-level request or controller (for example, Page-Show). If you configure pagecaching for the page itself, or any component within that page, the setting is applied for the response of the __SYSTEM__Page-Render and __SYSTEM__Page-Serialize request. Thus this setting affects the pagecaching for the whole page, and anything within it, but doesn’t affect anything outside of it like the controller that initiated the rendering.

If different pagecache settings are supplied for different components of the page, the response takes on the shortest of the pagecache settings. For example, if you have a page where:

  • Component A has a 1-minute relative pagecaching.
  • Component B has a 1-hour relative pagecaching.
  • Component C has a 1-day relative pagecaching.

The final calculated relative pagecaching is the minimum of these three values (1 minute).

Let’s look at an excerpt of the component type implementation that corresponds to component B to see how it instruments the response for a 1-hour pagecaching. Assuming this component presents personalized information (for example, based on a combination of pricebook, promotion, sorting rule, and A/B test segments), then you must account for that by instrumenting the response with the corresponding vary-by. For more information, refer to the B2C Commerce Script documentation.

module.exports.render = function(context) {
   // do your business logic
   ...

   // instruct 1 hour personalized relative pagecache
   var expiry = new Date();
   expiry.setHours(expiry.getHours() + 1);
   response.setExpires(expiry);
   response.setVaryBy('price_promotion');

   // create markup
   return ...;
}

If you use dw.util.Template.render() to produce your markup, as SFRA does, then you must not use <iscache> in your template. Use response.setExpires(), as shown above, because the dw.util.Template class is suited only for string or markup generation and not response modification.

If you don’t supply any pagecache setting for your page and none of its components (for example, neither the page type implementation itself nor component A, B, or C related component type implementations configure a pagecache setting) then the page isn’t going to be pagecached at all. As soon as at least one of these building blocks within a page supplies a pagecache setting, pagecaching happens.

Because the visibility fingerprint is also factored in to the pagecache key of a page rendering or serialization (in shape of a query parameter vf of the cachable __SYSTEM__Page-Render and __SYSTEM__Page-Serialize), there’s no need for the page or component type implementation to care about page or component visibility rules, that take for example customer groups into consideration, as this is already automatically handled for you.

Best Practices 

If your page or component requires a minimum pagecache setting, supply it in your page or component type implementation. Component types require extra care as this pagecache setting is affecting the pagecache of the entire page and not just the component alone.

If your page or component doesn’t strictly need any specific pagecache setting - don’t supply it in your page or component type implementation because it relies on the pagecache instrumentation supplied by the other components that reside on the page.

This is subject to change as your merchant adjusts which components are plugged into a page over time.

Note

For a page type that doesn’t supply a pagecache setting, carefully think about this decision as this means that a page’s components determine if the page is pagecached or not.

If the content of a component can’t be affected by the pagecache settings of the page, other components, or must not be pagecached at all, you must move such content into a remote include to separate it from the rest of the page. The remotely included controller (and its respective template) can then use a pagecache setting as it fits its business purpose. For example, a product tile component that renders the product’s inventory would likely want to include this inventory state in a remote include manner so that the current inventory can always be shown while the remainder of this component can still be pagecached.

Page Type, Component Type, and Custom Attribute Editor Cache Times 

How long page types, component types, and custom attribute editors are cached in memory depends on whether the environment is sandbox, development, staging, or production.

To refresh the page types, component types, and custom attribute editors on a production environment, or to refresh them immediately on other environments, a code version switch must occur. When you develop new page types, component types, or custom attribute editors, or modify existing ones, and then replicate them to a production environment, a best practice is to issue an event for a code version switch so that the page types, component types, and custom attribute editors are immediately refreshed.

EnvironmentCache Refresh Interval
Sandbox5 seconds or on code version switch
Development5 seconds or on code version switch
Staging5 seconds or on code version switch
ProductionOn code version switch

Publish a Page Designer Page 

Page Designer pages are rendered from a controller and incorporated into the storefront.

Render Page Designer Pages from a Controller 

Use the dw.experience.PageMgr API to render pages.

For more information, see PageMgr API

Pages are rendered together with their components. You can’t render components separately.

This is the basic code for the page rendering process:

...
var content = PageMgr.renderPage(cid, null);
// emit content (for example, write to response)
...

You can put the call to PageMgr.renderPage()in its own controller or add it to an existing controller. To take advantage of SEO URL features, a best practice is to add it to the Page-Show controller.

Page rendering occurs within a nested remote include. If you want to use external parameters in the rendering process, for example, parameters accessible by the controller that calls PageMgr.renderPage(), pass the parameters to the rendering process using a parameters string. For example:

...var params = { /* add your custom parameters here */ };
var content = PageMgr.renderPage(cid, JSON.stringify(params));
// emit content (e.g. write to response)
...

The remote include that does the rendering doesn't allow passing response headers or cookies set during rendering to the request that called PageMgr.renderPage(). If you need to adjust the response, do so outside of the page rendering process.

Incorporate a Page Designer Page into the Storefront 

You can use a storefront controller to create a URL to Page Designer pages. You can use the URL, for example, to link to a page from a marketing email. You can also use the URL to add a link to the page from the global header or footer or main navigation of the storefront.

Create the storefront controller using the dw.experience API. The following example controller, PDPage.js, exports a Show function that passes in the ID of the Page Designer page. The controller checks whether to make the page visible based on schedule and customer group. If the page is visible, the controller renders it. Otherwise, the user is redirected to the home page.

For clarity, this example shows a separate controller dedicated to rendering Page Manager pages. A best practice is to include the call to PageMgr.renderPage in the Page-Show controller to take advantage of SEO URL features.

Note

PDPage.js
'use strict';

var PageMgr = require('dw/experience/PageMgr');
var URLUtils = require('dw/web/URLUtils');

exports.Show = function ()
{
    var page = PageMgr.getPage(request.httpParameterMap.cid.stringValue);

    // Render only if the page is currently visible (as driven by the
    // online flag for scheduling and customer segmentation that the merchant
    // configured for the page)
    if (page != null && page.isVisible())
    {
        response.writer.print(PageMgr.renderPage(page.ID, ""));
    }
    else
    {
        response.redirect(URLUtils.httpsHome().toString());
    }
};
exports.Show.public = true;

When this controller is uploaded to the site, any Page Designer page can be accessed by passing in its page ID in a URL as follows:

{\<urlToTheStorefront>}/PDPage-Show?cid={\<pageID>}

For example, if you have a page with ID loyalty-rewards, the following URL accesses that page:

{\<urlToTheStorefront>}/PDPage-Show?cid=loyalty-rewards

To add a link to your page from the storefront's global header or footer, add the URL to the ISML file for the header or footer. For example:

<a href=$url('PDPage-Show','cid','loyalty-rewards')"title="Loyalty Program">Loyalty Program</a>

You could also add a link to the page from the storefront main navigation using the Business Manager to set up a new category. Configure the alternative URL for the category as follows:

$url('PDPage-Show','cid','loyalty-rewards')$

Mock Component Placeholders 

When a merchant drags a component onto a Page Designer page in edit mode, a mock component placeholder displays until the merchant configures the component attributes. Mock component placeholders are also used when a merchant creates a localized page based on another page or when a page doesn't render because it's missing attributes.

The mock component placeholder signals to the merchant that the component isn’t yet fully configured or has an error condition. The merchant can move the mock component and edit it like a regular component. The mock component displays only in edit mode.

A mock component uses the following values:

  • The name of the component type as configured in its meta definition file.
  • The thumbnail image for the component type provided in a custom cartridge for Page Designer UI artifacts or, if no custom thumbnail image is provided, the generic Page Designer component thumbnail image.

This example shows what a page looks like with four mock components, one for a headline banner and three for product tiles.

Screen showing four mock components.

The HTML wrapper is always div. Mock components use the following two classes:

  • sfdc-component-mock
  • sfdc-component-{typeID}-mock, where {typeID} is the ID of the component type. Component type ID values include dots, for example, assets.banner. When used in the class name, the dot is replaced with a hyphen, for example, assets-banner. For example, for a component type with ID assets.banner, the class is sfdc-component-assets-banner-mock.

To customize how a mock component placeholder displays, you can apply custom CSS to the mock component class. For example, this CSS file, named banner-mock.css, specifies that mock components using the class sfdc-component-assets-banner-mock have a background-color of lightblue.

**banner-mock.css** .sfdc-component-assets-banner-mock {
  background-color: lightblue;
}

Render a JSON View of a Page Designer Page 

You can expose the data assembled by a page and its components in JSON format. This process can be useful if you want to expose the page to external services to implement native mobile apps or custom heads.

Use the serializePage method of the dw.experience.PageMgr API to render the page in JSON. For more information about PageMgr, see PageMgr API.

The method to expose JSON has the following signatures:

String : serializePage(String pageID, String parameters)
String : serializePage(String pageID, Map<String,Object> aspectAttributes, String parameters)

For example, the following code uses the serializePage method to render a page in JSON.

.../cartridge/controllers/Page.js
function json() {
   response.setContentType('application/json');

   var page = PageMgr.getPage(request.httpParameterMap.cid.value);

   if (page == null)
   {
      response.setStatus(404);
   }
   else
   {
      if (!page.hasVisibilityRules())
      {
         var ONE_WEEK = new Date().getTime() + 7 * 24 * 60 * 60 * 1000;
         response.setExpires(ONE_WEEK);
      }

      if (page.isVisible())
      {
         response.writer.print(PageMgr.serializePage(page.ID, null));
       }
       else
       {
         response.setStatus(404);
       }
    }
}

In this example, the page has three regions:

  • Header: banner
  • Main: category tile and a 2x2 layout grid that holds product and category tiles
  • Footer: banner

The resulting JSON includes content structured as follows:

  • id: String: The ID of the page or component.
  • data: Map<String, Object>: Content attributes.
  • custom: Map <String, Object>: Additional data that custom code assembles.
  • regions: Region: The region model representing the component hierarchy:
    • id: String: The ID of the region.
    • components: List<StructuredContent>: All visible components assigned to the region in order.
{
  "id": "mypage",
  "data": {
    "attr1": "foo"
  },
  "regions": [
    {
      "id": "header",
      "components": [
        {
          "id": "j2q3eö9fl3inil",
          "data": {
            "title": "New Arrivals",
            "bannerimage": "/catalog/newarrivals.jpg"
          }
        }
      ]
    },
    {
      "id": "main",
      "components": [
        {
          "id": "4wso9j3fwohj3o8",
          "data": {
            "category": "new-arrivals",
            "image": "hotstuff.png"
          }
        },
        {
          "id": "e4g5vuftzue457z4",
          "data": {
            "layout": "2x2"
          },
          "regions": [
            {
              "id": "tiles",
              "components": [
                {
                  "id": "3gfn3w820uz4",
                  "data": {
                    "product": "9237918273"
                  }
                },
                {
                  "id": "3v5zb5r7i547u",
                  "data": {
                    "product": "2374984579"
                  }
                },
                {
                  "id": "ev5z45z3z34235",
                  "data": {
                    "category": "mens-shoes"
                  }
                },
                {
                  "id": "vurz3tv35zu34z",
                  "data": {
                    "category": "womens-shoes"
                  }
                }
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "footer",
      "components": [
        {
          "id": "87wa3tr897g38s",
          "data": {
            "title": "Autumn Collection",
            "bannerimage": "/promo/autumn/collection.jpg"
          }
        }
      ]
    }
  ]
}

If your page type or component types have custom code that you want to include in the exposed JSON, use the serialize(context) function in addition to therender(context) function in the JavaScript for the page or component, as in the following example.

.../cartridge/experience/components/producttile.js
module.exports.serialize = function (context) {
    var product = context.content.product;
    var images = product.getImages('large');
    var productImage = null;
    if (images.iterator() && images.iterator().hasNext()) {
        productImage = images.iterator().next();
    }

    return {
        "url" : URLUtils.url('Product-Show', 'pid', product.ID).toString(),
        "image" : productImage.getAbsURL().toString();
    };
}

The JSON returned includes the output of the custom code as a custom property of the page or component as in this example.

{
    "id": "mypage",
    "data": {
        "attr1": "foo"
    },
    "regions": [
        {
            "id": "header",
            "components": [
                {
                    "id": "j2q3eö9fl3inil",
                    "data": {
                        "title": "New Arrivals",
                        "bannerimage": "/catalog/newarrivals.jpg"
                    }
                }
            ]
        },
        {
            "id": "main",
            "components" : [
                {
                    "id": "4wso9j3fwohj3o8",
                    "data": {
                        "category": "new-arrivals",
                        "image": "hotstuff.png"
                    }
                },
                {
                    "id": "e4g5vuftzue457z4",
                    "data": {
                        "layout": "2x2"
                    },
                    "regions": [
                        {
                            "id": "tiles",
                            "components": [
                                {
                                    "id": "3gfn3w820uz4",
                                    "data": {
                                        "product": "9237918273"
                                    },
                      **  "custom": {
                                        "url" : "https://.../Product-Show?pid=9237918273",
                                        "image" : "https://.../iphone6.jpg"
                                    }
**                             },
                                {
                                    "id": "3v5zb5r7i547u",
                                    "data": {
                                        "product": "2374984579"
                                    },
                          **"custom": {
                                        "url" : "https://.../Product-Show?pid=2374984579",
                                        "image" : "https://.../galaxyS8.jpg"
                                    }**
                                },
                                {
                                    "id": "ev5z45z3z34235",
                                    "data": {
                                        "category": "mens-shoes"
                                    }
                                },
                                {
                                    "id": "vurz3tv35zu34z",
                                    "data": {
                                        "category": "womens-shoes"
                                    }
                                }
                            ]
                        }
                    ]
                }
            ]
        },
        {
            "id": "footer",
            "components" : [
                {
                    "id": "87wa3tr897g38s",
                    "data": {
                        "title": "Autumn Collection",
                        "bannerimage": "/promo/autumn/collection.jpg"
                    }
                }
            ]
        }
    ]
}