Skip to main content

Example: Creating a Smart Discovery template

This example shows you how to create a slider template for a Smart Discovery.

Creating the template involves:

  • Building a slider element for the discovery. The element code uses Mustache variables for dynamically populating the slider with Smart Discovery data.

  • Fetching the Smart Discovery data for the page the visitor is currently viewing. You use the Smart Discovery API for fetching the data.

  • Defining content options for setting the display title and page type of the discovery.

You create the template in the Frosmo Control Panel.

The following figure shows an example of the final discovery slider on a category page.

Smart Discovery slider on a category page
Figure: Smart Discovery slider on a category page
note

This example assumes that Frosmo Search & Smart Discovery is correctly set up on the site, and that the platform has generated the Smart Discovery data for categories.

tip

If you're trying out this example on a production site, but do not want the example to interfere with production content, use a workspace.

To create the Smart Discovery slider template:

  1. In the Frosmo Control Panel, in the sidebar, select Modifications.

  2. Select the Templates tab.

  3. Click Create template.

  4. Define the following settings:

    • Name: Enter "Smart Discovery slider".

    • Export content: Leave this field to Automatic.

    • Minified: Leave this field to Enabled.

    • Content: Enter the following content for the template.

      Content for the template
      <div class="smart-discovery">
      <div class="smart-discovery-header">
      <h3 class="smart-discovery-header-title">{{title}}</h3>
      <div class="smart-discovery-header-divider"></div>
      </div>
      <div class="smart-discovery-container">
      <div class="smart-discovery-collections">
      {{#collections}}
      <div class="smart-discovery-collection-container">
      <a class="smart-discovery-collection" href="{{url}}" data-collection-id="{{id}}">
      <img alt="{{{title}}}" class="smart-discovery-collection-image" src="{{image}}" loading="lazy" />
      <div class="smart-discovery-collection-name">{{{title}}}</div>
      </a>
      </div>
      {{/collections}}
      </div>
      <div class="slider-control slider-control-prev"></div>
      <div class="slider-control slider-control-next"></div>
      </div>
      </div>

      <style>
      .smart-discovery {
      display: flex;
      flex-direction: column;
      margin: 20px 0px;
      }

      .smart-discovery-header {
      display: flex;
      align-items: center;
      margin-bottom: 18px;
      }

      .smart-discovery-header-title {
      font-size: 20px;
      margin-bottom: 0;
      }

      .smart-discovery-header-divider {
      flex-grow: 1;
      background: #ebebeb;
      height: 2px;
      }

      .smart-discovery-header-title+.smart-discovery-header-divider {
      margin-left: 16px;
      }

      .smart-discovery-container {
      position: relative;
      height: 180px;
      max-height: 220px;
      }

      .smart-discovery-collections {
      display: flex;
      overflow-x: scroll;
      overflow-x: hidden;
      scroll-snap-type: x mandatory;
      -webkit-overflow-scrolling: touch;
      scroll-behavior: smooth;
      -ms-overflow-style: none;
      scrollbar-width: none;
      }

      .smart-discovery-collection {
      position: relative;
      margin: 0.5em;
      padding: 0.2em;
      display: flex;
      flex-direction: column;
      align-items: center;
      background-color: white;
      text-decoration: none;
      border: 1px solid rgba(0, 0, 0, 0.1);
      box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
      scroll-snap-align: start;
      }

      .smart-discovery-collection:hover {
      box-shadow: 0 2px 4px rgb(0 0 0 / 10%);
      opacity: 1.5;
      text-decoration: underline;
      text-decoration-color: black;
      }

      .smart-discovery-collection-image {
      object-fit: contain;
      max-width: 120px;
      max-height: 120px;
      min-width: 80px;
      }

      .smart-discovery-collection-name {
      text-align: center;
      max-height: 2.8em;
      min-height: 2.8em;
      font-weight: 400;
      font-size: 13px;
      color: #303030;
      margin: 0.1em;
      line-height: 1.4em;
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      text-transform: uppercase;
      font-weight: bold;
      }

      .slider-control {
      position: absolute;
      top: 50%;
      cursor: pointer;
      opacity: 0;
      z-index: 1;
      display: none;
      box-shadow: 2px 2px 8px 0px rgba(0, 0, 0, 0.5);
      background: white;
      padding: 0.5em;
      }

      .slider-control-visible {
      opacity: 1;
      display: block;
      }

      .slider-control-visible:hover {
      opacity: 0.5;
      }

      .slider-control-prev {
      left: -2em;
      transform: translate(-50%, -50%);
      }

      .slider-control-prev:after {
      content: url('https://d2oarllo6tn86.cloudfront.net/message_files/32/387/122/icon-arrow-left.png');
      }

      .slider-control-next {
      right: -1.5em;
      transform: translate(50%, -50%);
      }

      .slider-control-next:after {
      content: url('https://d2oarllo6tn86.cloudfront.net/message_files/32/387/123/icon-arrow-right.png');
      }

      /** Responsive containers */

      .smart-discovery-collection-container {
      min-width: 45%;
      max-width: 45%;
      }

      @media (min-width: 700px) {
      .smart-discovery-collection-container {
      min-width: 33.5%;
      max-width: 33.5%;
      }
      }

      @media (min-width: 850px) {
      .smart-discovery-collection-container {
      min-width: 17.5%;
      max-width: 17.5%;
      }
      }

      @media (min-width: 1024px) {
      .smart-discovery-collection-container {
      min-width: 14.3%;
      max-width: 14.3%;
      }
      }
      </style>

      <script>
      var controlVisibleClass = 'slider-control-visible';
      var context = positionData.getMessageInstance().getElements()[0];

      var controlPrev = easy.domElements.selector('.slider-control-prev', context)[0];
      var controlNext = easy.domElements.selector('.slider-control-next', context)[0];
      var slider = easy.domElements.selector('.smart-discovery-collections', context)[0];

      /**
      * Get the visible width of the slider (page width).
      */
      function getSliderWidth() {
      return slider.getBoundingClientRect().width;
      }

      /**
      * Scroll the slider by amount (px).
      */
      function scrollSlider(amount) {
      var scrollTarget = slider.scrollLeft + amount;
      slider.scrollLeft = scrollTarget;
      }

      /**
      * Move the slider backwards.
      */
      function sliderBackwards() {
      var pageSize = getSliderWidth();
      scrollSlider(-pageSize);
      }

      /**
      * Move the slider forwards.
      */
      function sliderForwards() {
      var pageSize = getSliderWidth();
      scrollSlider(pageSize)
      }

      /**
      * Handle control visibility when the slider element is scrolled.
      */
      function sliderScroll(event) {
      controlPrev.classList.add(controlVisibleClass);
      controlNext.classList.add(controlVisibleClass);

      if (slider.scrollLeft <= 25) {
      controlPrev.classList.remove(controlVisibleClass);
      }
      if ((slider.scrollLeft + 25) >= slider.scrollWidth - getSliderWidth() || slider.scrollWidth === getSliderWidth()) {
      controlNext.classList.remove(controlVisibleClass);
      }
      }

      // Bind the buttons and scroll event.
      easy.domEvents.on(controlPrev, 'click', sliderBackwards);
      easy.domEvents.on(controlNext, 'click', sliderForwards);
      easy.domEvents.on(slider, 'scroll', sliderScroll);

      // Force a scroll event on load. Calculates which buttons should show.
      sliderScroll();
      </script>

      The content defines the HTML and CSS code for the slider. The HTML includes multiple Mustache tags, which need to be replaced with specific values in the final rendered content. The content also defines some JavaScript for handling the slider controls.

      Feel free to edit the content to adapt the slider to your site's layout, style, and functionality.

    • Content prerenderer: Enter the following content prerenderer for the template.

      Content prerenderer for the template
      // UTILITY FUNCTIONS

      /**
      * Get the unique categories for the current page from the Frosmo Platform product impression event sent from the page.
      *
      * @returns {string[]} Unique categories for the current page
      */
      function getUniqueCategoriesFromImpressions() {
      return promiseFromEvent('frosmo_product_impression', 'once').then(function (eventData) {
      // The first item of the eventData array contains the items tracked on the page.
      return getUniqueValuesFromArray(eventData[0], 'category');
      });
      }

      /**
      * Converts an event into a Promise that resolves the next time the event fires.
      *
      * @param {string} event - Event name
      * @param {string} type - Event type
      * @returns {Promise} Promise that is resolved with the event arguments
      */
      function promiseFromEvent(event, type) {
      if (type === void 0) {
      type = 'once';
      }

      return new easy.Promise(function (resolve) {
      easy.events[type](event, function () {
      let args = [];

      for (let _i = 0; _i < arguments.length; _i++) {
      args[_i] = arguments[_i];
      }

      resolve(args);
      });
      });
      }

      /**
      * Get the unique values for a single property in an array of objects.
      *
      * @param {Object[]} sourceArray - Array of objects whose property values to get
      * @param {string} property - Name of the property whose values to get
      * @returns {Object[]} Unique property values sorted in descending order by count
      */
      function getUniqueValuesFromArray(sourceArray, property) {
      // Reduce the source array to an array of unique property values with counts.
      const uniqueCategoriesWithCounts = easy.utils.reduce(sourceArray, function (accumulator, item) {
      if (!item[property]) {
      return accumulator;
      }

      // Find the current item's property value in the accumulated array.
      const existingValue = easy.utils.find(accumulator, function (accumulatorItem) {
      return accumulatorItem.id === item[property];
      });

      // If the property value already exists, simply increase its count by 1.
      if (existingValue) {
      existingValue.count = existingValue.count + 1;
      return accumulator;
      }

      // If the property value does not exist, add it to the accumulated array.
      accumulator.push({
      id: item[property],
      count: 1
      });

      return accumulator;
      }, []).sort(function (a, b) {
      return b.count - a.count;
      });

      // Remove the counts, and only return the property values.
      return uniqueCategoriesWithCounts.map(function (category) {
      return category.id;
      });
      }

      // BUSINESS FUNCTIONS

      /**
      * Build the Smart Discovery API request, fetch the Smart Discovery data, and render the Smart Discovery.
      *
      * @returns {void}
      */
      function createSmartDiscovery() {
      const pageType = templateInstance.options.pageType;
      const apiUrl = `${easy.api.getApiUrl('smartDiscovery')}`;
      const apiParameters = {
      method: 'fetch',
      origin: easy.config.api.origin,
      pageType: pageType,
      context: '',
      cookieId: easy.uid.get(),
      };

      // Set the page context for a category page, and fetch and render the Smart Discovery.
      if (pageType === 'category') {
      getUniqueCategoriesFromImpressions().then(function (categoryItems) {
      if (categoryItems.length === 0) {
      return;
      }

      apiParameters.context = frosmo.JSON.stringify({
      page: {
      type: pageType,
      category: {
      categories: [categoryItems[0]],
      }
      }
      });

      fetchSmartDiscoveryData(apiUrl, apiParameters);
      })

      return;
      }

      // Set the page context for the home page, and fetch and render the Smart Discovery.
      if (pageType === 'home') {
      apiParameters.context = frosmo.JSON.stringify({
      page: {
      type: pageType
      }
      });

      fetchSmartDiscoveryData(apiUrl, apiParameters);

      return;
      }
      }

      /**
      * Fetch the Smart Discovery data with the Smart Discovery API, and render the Smart Discovery.
      *
      * @param {string} apiUrl - URL for the Smart Discovery API request
      * @param {Object} apiParameters - Query parameters for the Smart Discovery API request
      * @returns {void}
      */
      function fetchSmartDiscoveryData(apiUrl, apiParameters) {
      frosmo.easy.fetch.get(easy.utils.addQueryString(apiUrl, apiParameters)).then(renderSmartDiscovery);
      }

      /**
      * Render the Smart Discovery.
      *
      * @param {Object} response - Smart Discovery data to render
      * @returns {void}
      */
      function renderSmartDiscovery(response) {
      const collections = response.data[0].items.map(item => {
      // Build the search results page URL for the collection based on the collection's facets.
      // The facets are the attributes with values on which the collection is based, such as brand + category for "Nike Shoes".
      // In this example, the URL syntax is "/search?attribute1=value&attribute2=value...".
      // @todo Update this for your site's URL syntax.
      if (!item.facets) {
      item.url = '/search';
      return item;
      }
      // Build the URL query parameters from the facets.
      const query = Object.entries(item.facets).map(([key, value]) => `${key}=${value}`).join('&');
      item.url = '/search?' + query;
      return item;
      });

      const options = {
      title: templateInstance.options.title,
      collections: collections
      };

      // Render the template content with the content options.
      templateInstance.render(options);
      }

      createSmartDiscovery();

      The content prerenderer:

      1. Fetches the Smart Discovery data by calling the Smart Discovery API.

      2. Builds the final content options with which to render the template content. These are the values for the Mustache tags in the content. The values include the Smart Discovery title, which comes from the title content option set in the modification variation, and the details of the Smart Discovery collections, which the prerenderer extracts from the Smart Discovery data.

      3. Renders the template content with the final content options. The prerenderer uses the facets of a collection to build the search query URL for the collection.

      note

      This example was designed to work on the Frosmo retail demo site. When trying out the example on your own site, check and update at least the prerenderer code tagged with a @todo comment.

    • Content options schema: Enter the following content options schema for the template.

      Content options schema for the template
      {
      "type": "object",
      "title": "",
      "properties": {
      "title": {
      "type": "string",
      "title": "Smart Discovery title"
      },
      "pageType": {
      "type": "string",
      "title": "Smart Discovery page type",
      "enum": [
      "category",
      "home"
      ],
      "enumNames": [
      "Category",
      "Home"
      ],
      "default": "category"
      }
      },
      "required": [
      "title",
      "pageType"
      ]
      }

      The schema describes the following content options, which must be set in the content options UI of the modification variation:

      • Smart Discovery title (title)

      • Smart Discovery page type (pageType)

      The template uses the content options to create the final slider element.

    • Content options UI schema: To further customize the content options UI, enter the following content options UI schema for the template.

      Content options UI schema for the template
      {
      "title": {
      "ui:placeholder": "Enter a display title for the Smart Discovery"
      },
      "pageType": {
      "ui:placeholder": "Select the page type of the Smart Discovery"
      }
      }

      The UI schema defines placeholder texts for the Smart Discovery title and Smart Discovery page type fields.

    The following figure shows the complete template in the Control Panel. Click the figure to view a larger version.

    Settings for the Smart Discovery slider template

    For more information about the settings, see Creating and editing a template.

  5. Use the content options UI preview at the bottom of the editor to test that the content options schema and content options UI schema are correctly defined.

    The preview is fully interactive, so any changes you make to the schemas are automatically reflected in the preview. You can also set the content option in the preview as you would in the actual content options UI, and you can see a preview of the JSON object generated based on the set value.

    The following figure shows the content options UI preview for template.

    Content options UI preview for the Smart Discovery slider template
  6. Click Save.

You have created the template. You can now use the template in a modification to create a Smart Discovery slider for the site.

For a practical example where you can use the template, see Example: Creating a Smart Discovery for category pages.