allBlogsList

Implementing SFCC Page Designer with the PWA Kit

Discover how Page Designer works with PWA

Page Designer has been a great tool for business teams to create eCommerce experiences with an easy drag-and-drop interface. With Salesforce rolling out the PWA Kit and Manager Runtime and adding them as part of their offerings to customers, I wanted to explore how we could integrate Page Designer with the PWA Kit. So here I put together the process and demonstration at the end on how the integration works.

  1. OCAPI and Commerce API do not provide an API for Page Designer. Therefore, in order to integrate page designer with PWA, we will need to create a custom endpoint to feed page designer data to the PWA application.

  2. To start you need to install ocapi_hooks_collection cartridge which can be found under SalesforceCommerceCloud account. Quick overview on the cartridge. The cartridge extends OCAPI to allow for custom APIs which can be very helpful when implementing a headless solution. The way the cartridge works is by creating a new custom object and having each new custom API as records:

    SFCC Page Designer
    Then, using OCAPI hook: dw.ocapi.shop.custom_object.modifyGETResponse route the calls to the different controllers that would serve the custom APIs 

    Then, we can utilize customObject GET endpoint /custom_objects/{object_type}/{key} to make calls to the different custom APIs. Example: /custom_objects/CustomApi/page-designer?c_pages=homepage 

  3. Utilizing the custom page-designer API, we can retrieve a serialized version of a page designer page like the homepage. Below you can see an example of a simple homepage with a single region and two components:

    { 
        "_v": "21.10", 
        "_type": "custom_object", 
        "key_property": "ID", 
        "object_type": "CustomApi", 
        "c_result": { 
            "pages": [ 
                { 
                    "id": "homepage", 
                    "type_id": "storePage", 
                    "data": {}, 
                    "regions": [ 
                        { 
                            "id": "main", 
                            "components": [ 
                                { 
                                    "id": "bc8f63c7a01a0b2b2a900c86db", 
                                    "type_id": "pwa_components.parallaxBanner", 
                                    "data": { 
                                        "image": { 
                                            "path": "/images/homepage/dmitriy-ilkevich-437760-unsplash-parallax.jpg", 
                                            "focal_point": { 
                                                "x": 0.29, 
                                                "y": 0.52 
                                            }, 
                                            "meta_data": { 
                                                "height": 900, 
                                                "width": 1900 
                                            }, 
                                            "fullUrl": "https://zzhc-003.sandbox.us01.dx.commercecloud.salesforce.com/on/demandware.static/-/Library-Sites-RefArchSharedLibrary/default/dwd4557f98/images/homepage/dmitriy-ilkevich-437760-unsplash-parallax.jpg" 
                                        }, 
                                        "ctaStyle": "secondary", 
                                        "heading": "Autumn Vibes", 
                                        "textAlignment": "center", 
                                        "category": "newarrivals", 
                                        "subheading": "Just Arrived" 
                                    } 
                                }, 
                                { 
                                    "id": "9860066d2394aa617f6c6bb66e", 
                                    "type_id": "pwa_components.parallaxBanner", 
                                    "data": { 
                                        "image": { 
                                            "path": "/images/homepage/serrah-galos-494279-unsplash-parallax.jpg", 
                                            "focal_point": { 
                                                "x": 0.72, 
                                                "y": 0.53 
                                            }, 
                                            "meta_data": { 
                                                "height": 900, 
                                                "width": 1900 
                                            }, 
                                            "fullUrl": "https://zzhc-003.sandbox.us01.dx.commercecloud.salesforce.com/on/demandware.static/-/Library-Sites-RefArchSharedLibrary/default/dw205b24d8/images/homepage/serrah-galos-494279-unsplash-parallax.jpg" 
                                        }, 
                                        "ctaStyle": "primary", 
                                        "heading": "Ethnic Sweaters", 
                                        "textAlignment": "left", 
                                        "category": "womens-clothing-jackets", 
                                        "subheading": "Trendy. Comfy. Beautiful" 
                                    } 
                                } 
                            ] 
                        } 
                    ] 
                } 
            ] 
        } 
    } 
    
  4. As you can see in the example above, I decided to create a new collection pwa_components and create a parallaxBanner component that will end up being built using react in PWA. The parallaxBanner component has several attributes that we will use to configure the react component (heading, subheading, text alignment…etc). 

  5. Another thing to note, the image object does not include the fullUrl as it's generated by PageMgr.serializePage(string, string). However, this is needed by the PWA because there’s no way for us to get the full image path with the cache token. So, I had to modify the response returned from PageDesigner serialization API and assign the full URL to it: 

    URLUtils.httpsStatic(URLUtils.CONTEXT_LIBRARY, null, component.data.image.path).toString(); 
    

    This is one example of altering the response but I’m sure different components might require additional alterations so it’s good to keep in mind that it’s possible. 

  6. Now that we have the page-designer custom API ready for us to utilize, we want to add the new API to the PWA: 
    + app/commerce-api/ocapi-page-designer.js 

    // This class allows integration with OCAPI Custom API Page Designer 
    // https://github.com/SalesforceCommerceCloud/ocapi_hooks_collection 
    import {checkRequiredParameters, createOcapiFetch} from './utils' 
    class OcapiPageDesigner { 
        constructor(config) { 
            this.fetch = createOcapiFetch(config) 
        } 
        async getSerializedPage(...args) { 
            const required = ['pageId'] 
            let requiredParametersError = checkRequiredParameters(args[0], required) 
            if (requiredParametersError) { 
                return requiredParametersError 
            } 
            let { 
                parameters: {pageId} 
            } = args[0] 
            return this.fetch( 
                `custom_objects/CustomApi/page-designer?c_pages=${pageId}`, 
                'GET', 
                args, 
                'getSerializedPage' 
            ) 
        } 
    } 
    export default OcapiPageDesigner
    

    Extend app/commerce-api/index.js 

    import OcapiPageDesigner from './ocapi-page-designer' 
    const apis = { 
                shopperCustomers: sdk.ShopperCustomers, 
                shopperBaskets: OcapiShopperBaskets, 
                shopperGiftCertificates: sdk.ShopperGiftCertificates, 
                shopperLogin: sdk.ShopperLogin, 
                shopperOrders: OcapiShopperOrders, 
                shopperProducts: sdk.ShopperProducts, 
                shopperPromotions: sdk.ShopperPromotions, 
                shopperSearch: sdk.ShopperSearch, 
                + shopperPageDesigner: OcapiPageDesigner, 
                adyen: OcapiAdyen 
            } 
            const apiConfigs = { 
                shopperCustomers: {api: sdk.ShopperCustomers, canLocalize: false}, 
                shopperBaskets: {api: OcapiShopperBaskets, canLocalize: false}, 
                shopperGiftCertificates: {api: sdk.ShopperGiftCertificates, canLocalize: true}, 
                shopperLogin: {api: sdk.ShopperLogin, canLocalize: false}, 
                shopperOrders: {api: OcapiShopperOrders, canLocalize: true}, 
                shopperProducts: {api: sdk.ShopperProducts, canLocalize: true}, 
                shopperPromotions: {api: sdk.ShopperPromotions, canLocalize: true}, 
                shopperSearch: {api: sdk.ShopperSearch, canLocalize: true}, 
                + shopperPageDesigner: {api: OcapiPageDesigner, canLocalize: true}, 
                adyen: {api: OcapiAdyen, canLocalize: false} 
            } 
    

    Add a page designer hook that can be used to retrieve any page designer serialized page. In our case, we’ll only utilize it for the homepage 
    + app/commerce-api/hooks/usePageDesigner.js 

    import {useState} from 'react' 
    import {useCommerceAPI} from '../utils' 
    const usePageDesigner = () => { 
        const api = useCommerceAPI() 
        const [state, setState] = useState({page: 0}) 
        return { 
            ...state, 
            // Check if the page designer response is loaded 
            get loaded() { 
                return !!state.page 
            }, 
            /** 
             * Retrieves page designer serialized page from api based on pageId 
             * 
             * @param {input} string 
             */ 
            async getPage(pageId) { 
                const response = await api.shopperPageDesigner.getSerializedPage({ 
                    parameters: { 
                        pageId: pageId 
                    } 
                }) 
                const pages = response.c_result.pages 
                if (pages.length) { 
                    setState({page: pages[0]}) 
                } 
            }
        } 
    } 
    export default usePageDesigner 
    
  7. Now that we have the pageDesigner hook setup and we’re able to get a serialized response for a page, we’re ready to create the react components that will form the page designer page: 

  • I started with a PdPage component. The component expects two props: 

    • pageType which refers to page designer page type id (storePage, productListingPage, productDetailPage). In our example, we’re only using storePage, but the class can be extended to handle other pageTypes.  

    • Regions a list of regions that are associated with the page

              This will simply route to the different pageTypes: 

import React from 'react' 
import PropTypes from 'prop-types' 
import StorePage from './pages/storePage' 
const PdPage = (props) => { 
    const {pageType, regions} = props 
    if (pageType === 'storePage') { 
        return  
    } else { 
        return '' 
    } 
} 
PdPage.displayName = 'pdPage' 
PdPage.propTypes = { 
    /** 
     * PD Page Type 
     */ 
    pageType: PropTypes.string.isRequired, 
    /** 
     * A list of regions that form the PD page 
     */ 
    regions: PropTypes.array.isRequired 
} 
export default PdPage 
  • StorePage component is the layout that would mimic the page designer layout. It expects a list of regions. In our example, it’s simply a single region called MainRegion: 
import React from 'react' 
import {VStack} from '@chakra-ui/react' 
import PropTypes from 'prop-types' 
import MainRegion from './regions/main-region' 
const StorePage = (props) => { 
    const {regions} = props 
    return ( 
            {regions.map(function(region, index) { 
                if (region.id === 'main') { 
                    return  
                } else { 
                    return '' 
                } 
            })} 
    ) 
} 
StorePage.displayName = 'StorePage' 
StorePage.propTypes = { 
    /** 
     * A list of regions that form the PD page 
     */ 
    regions: PropTypes.array.isRequired 
} 
export default StorePage 
  • MainRegion will form the region layout with all the component. In the example below, we’re only handling a paralaxBanner component: 
import React from 'react' 
import PropTypes from 'prop-types' 
import ParallaxBanner from '../../components/parallax-banner' 
const renderComponent = (component, index) => { 
    switch (component.typeId) { 
        case 'pwa_components.parallaxBanner': 
            return (        
            ) 
        default: 
            return '' 
    } 
} 
const MainRegion = (props) => { 
    const {components} = props 
    return components?.map((component, index) => { 
        return renderComponent(component, index) 
    }) 
} 
MainRegion.displayName = 'StorePage' 
MainRegion.propTypes = { 
    /** 
     * A list of regions that form the PD page 
     */ 
    components: PropTypes.array.isRequired 
}  
export default MainRegion       

Below is a demonstration of the feature: 

Resources: 

PWA Kit: https://github.com/SalesforceCommerceCloud/pwa-kit 

OCAPI Hooks Collection: https://github.com/SalesforceCommerceCloud/ocapi_hooks_collection 

Render a JSON View of a Page Designer Page:  https://documentation.b2c.commercecloud.salesforce.com/DOC2/topic/com.demandware.dochelp/content/b2c_commerce/topics/page_designer/b2c_render_json_view.html?resultof=%22%72%65%67%69%6f%6e%22%20