Broadleaf Microservices
  • v1.0.0-latest-prod

Homepage Walkthrough

In this walkthrough, we’ll be examining how we built the commerce app’s homepage. Primarily, we’ll look at the different sections and what API calls were necessary to fill them out.

Homepage

Requests to Establish Application Context

We need to make a few requests before we start loading content specific to the homepage, or any other page for that matter. We’ll go into more detail about these elsewhere, but here’s a brief overview:

  1. Resolve the tenant and application

  2. Send a request to authenticate the user and determine permissions, whether as anonymous or signed-in

Additionally, there are some other requests that we can start sending to gather additional context but which do not prevent us from loading content:

  1. Check whether the request originated from the Admin as a preview-on-site request.

  2. Resolve the in-progress cart, if any, for the current user

    • We’ll either determine this from a previously added cookie for anonymous users or from an authenticated user’s ID

    • See the Cart Walkthrough

  3. Check whether the request originated from the Admin as a Customer Service Representative (CSR) request.

    • Let’s a CSR shop on the site as a guest or registered customer, i.e., impersonate a customer, to help resolve issues for a them

    • See the CSR Features Walkthrough

Filling out the Header

Now, we can start examining the where the content for the homepage comes from. Let’s start with the header. We see several subcomponents here from top-to-bottom, left-to-right:

Header

Let’s go into more detail on what API requests are used to load the content for each of those.

Locale Selector

Let’s examine the locale selector.

Header Locale Selector Expanded

When the Application is resolved, the response includes a list of allowed locales. We use those to populate this selector, with the default automatically selected. Once a locale is selected we reload the content of the page to grab all of the translations that are stored on the backend. We also update the static text in the app if there are messages for the new locale.

Note
The flag icons you see are configured in the frontend itself. The backend only responds with the locale codes, and it’s up to the you to decide how those should be represented.

Currency Selector

Let’s examine the currency selector.

Header Currency Selector Expanded

When the Application is resolved, the response includes a list of allowed currencies. We use those to populate this selector, with the default automatically selected. Once a currency is selected, we refetch any priceable entities on the page like products.

Like the locales for the selector, we get this from the resolved Application.

Header Logo

However, we don’t actually get the asset itself from the Application response: We get its metadata like alt-text, title and URL. The asset itself is managed by the Asset Services, and to fetch it we’ll usually send a request to the content resolution endpoint using the asset’s content URL. The content URL could point to CDN or to the content resolution endpoint in Asset Services.

Tip
Checkout the API docs for the application resolution request to see all of what it includes besides locales (like currencies) And see Tenant & Application Walkthrough.

Let’s take a look at the main site navigation menu.

Header Menu

We maintain this menu in the Admin, and derive it from the Menu Service. To retrieve it, we just need its name: HEADER_MENU. We have this setup to be configurable with a property HEADER_MENU_NAME, so it’s easy to change without getting into the code. The name is then set as a parameter called menuName.

Request

GET /api/menu/menu-hierarchy?menuName=HEADER_MENU

With the Commerce SDK
import { ClientOptions } from '@broadleaf/commerce-core';
import { MenuClient } from '@broadleaf/commerce-menu';

async function getHeaderMenu(options: ClientOptions) {
    const client = new MenuClient(options);
    return client.getMenuHierarchy(process.env.NEXT_PUBLIC_HEADER_MENU_NAME);
}
Response Payload
{
  "id": "01E00311DAH8RF0YQGXF0F10DM",
  "name": "HEADER_MENU",
  "submenu": [
    {
      "id": "01E0038S14SREB1KEKBGDP1K54",
      "parentMenuId": "01E00311DAH8RF0YQGXF0F10DM",
      "label": "Home",
      "url": "/",
      "displayOrder": 1000,
      "type": "LINK",
      "submenu": []
    },
    {
      "id": "01E003BFC2S6FJ1V8K46VY00VD",
      "parentMenuId": "01E00311DAH8RF0YQGXF0F10DM",
      "label": "Hot Sauces",
      "url": "/hot-sauces",
      "displayOrder": 2000,
      "type": "CATEGORY",
      "submenu": []
    },
    {
      "id": "01E003E5KJD9RR19K89WF412JG",
      "parentMenuId": "01E00311DAH8RF0YQGXF0F10DM",
      "label": "Merchandise",
      "url": "/merchandise",
      "displayOrder": 3000,
      "type": "CATEGORY",
      "submenu": [
        {
          "id": "01E003FSRDSNPQ1GBWVQ2R1N8G",
          "parentMenuItemId": "01E003E5KJD9RR19K89WF412JG",
          "parentMenuId": "01E00311DAH8RF0YQGXF0F10DM",
          "label": "Holiday Hats",
          "url": "/holiday-hats",
          "displayOrder": 1000,
          "type": "CATEGORY",
          "submenu": []
        }
      ]
    },
    {
      "id": "01E003J6D6WZ4M1932HR3F179M",
      "parentMenuId": "01E00311DAH8RF0YQGXF0F10DM",
      "label": "Clearance",
      "url": "/clearance",
      "displayOrder": 4000,
      "type": "CATEGORY",
      "submenu": []
    }
  ]
}
Note

To go into a little more detail, a Menu consists of several MenuItems. These MenuItems typically are configured with a link URL, display order, and a simple label. However, we can get even more complex than that. An item can have any of these types:

  • Link (default): Defines a basic hyperlink

  • Category: Defines a link to a Category browse page

    • This is useful to the frontend to know so that we can have special behavior

    • We use it to append the /browse context path to distinguish it from a product details link (managed via the CATEGORY_URL_PREFIX property)

  • Product: Defines a link to a Product detail page

    • Similarly, this is useful to the frontend, and we use it to prepend /details to distinguish it from a category link (managed via the PRODUCT_URL_PREFIX property)

  • Asset: Defines an item with an asset

  • Text: Defines an item as simple text

  • Page: Defines a link to a CMS managed page

    • Note that we currently do not provide our own CMS

Moreover, menu items can be setup to have sub-items so as to create submenus. In the case of our header menu, we have one example: Merchandise This is just one way to setup a sub-menu. Chekout Footer Navigation for another example.

Catalog Search Input

This input allows searching through the catalog.

Header Search

While typing, we’ve configured it to send requests to get type-ahead suggestions. These suggestions include not just keywords, but also categories and specific products related to the search term. Each of these suggestions are clickable, so that a product suggestion will take the user to that product’s detail page.

Header Search Typeahead

These requests are sent to the Catalog-Browse Service, which fronts the Search Services for commerce-facing requests. The request should include 2 parameters: query and config. Query is self-explanatory, but config specifies which TypeAheadConfiguration to use. We also include the X-Price-Context and X-Price-Info-Context headers so that pricing can be hydrated on the results without making a second request from the browser.

Request

GET /api/catalog-browse/search/catalog/suggest?query=hot&config=productTypeAhead

With the Commerce SDK
import { BrowseClient } from '@broadleaf/commerce-browse';
import { ClientOptions } from '@broadleaf/commerce-core';

async function getTypeAheadResults(options: ClientOptions) {
    const client = new BrowseClient(options);
    return client.searchSuggestions({
      query: 'hot',
      name: 'productTypeAhead',
    });
}
Price Context Header
{
  "locale": "en-US",
  "currency": "USD",
  "userTargets": [
    {
      "targetValue": "authUser.serviceId",
      "targetType": "CUSTOMER"
    }
  ],
  "attributes": {
    "webRequest": {
      "fullUrl": "https://www.heatclinic.com/",
      "pathname": "/",
      "userAgentType": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36 OPR/72.0.3815.320",
      "secure": true,
      "searchKeywords": ""
    }
  }
}
Price Info Context Header
{
  "priceLists": [
    {
      "id": "string",
      "name": "string",
      "type": "STANDARD|SALE|CONTRACT",
      "priority": 0,
      "currency": "USD|MXN|CAD|EUR|GBP|etc"
    }
  ],
  "skipDetails": true
}
Response Payload
{
  "suggestions": {
    "categories": [
      {
        "categoryNames": "Hot Sauces",
        "numResults": 2
      },
      {
        "categoryIds": "category1",
        "numResults": 2
      }
    ],
    "products": [
      {
        "id": "product4",
        "priceInfo": {
          "price": {
            "amount": 8.99,
            "currency": "USD"
          },
          "priceType": "basePrice",
          "priceTypeDetails": {
            "basePrice": {
              "type": "basePrice",
              "bestPrice": {
                "amount": 8.99,
                "currency": "USD"
              },
              "priceDetails": {}
            }
          }
        },
        "currency": "USD",
        "price": {
          "amount": 8.99,
          "currency": "USD"
        },
        "options": [],
        "variants": [],
        "includedProducts": [],
        "promotionalProducts": {},
        "name": "Hoppin' Hot Sauce",
        "description": "Tangy, ripe cayenne peppers flow together with garlic, onion, tomato paste and a hint of cane sugar to make this a smooth sauce with a bite. Wonderful on eggs, poultry, pork, or fish, this sauce blends to make rich marinades and soups.",
        "uri": "/hot-sauces/hoppin-hot-sauce",
        "primaryAssetContentUrl": "https://{demo-admin-host}/api/asset/content/Hoppin-Hot-Sauce-Bottle.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
        "primaryAssetAltText": "Bottle of Hoppin' Hot Sauce"
      }
    ]
  },
  "keyWords": [
    {
      "highlightedSuggestion": "<strong>Hot</strong> Sauce",
      "suggestion": "Hot Sauce"
    }
  ]
}
Note

A TypeAheadConfiguration contains the information such as

  • the type of the entity targeted (indexable type) such as a Product

  • the fields to highlight in the response (these are what we matched on)

  • the maximum number of suggestions to show

  • the entity properties to include in the response such as a product’s price and name

Once a query is submitted, we send it along to the Catalog-Browse Service again and then redirect to the Search Results page. That page includes sorting, filtering, and faceting on the results. By default, we have Apache Solr powering our search, and have it index the catalog. See the Search Results walkthrough for details

These are currently configured in the frontend itself as links to the sign-in and registration pages.

Header Account Links

The sign-in page lives on a separate domain for security reasons, but once signed-in, the user will be redirected back here.

Sign In Page

Likewise with the registration page:

Registration Page

When the form is submitted, it will redirect the user to the sign-in page so they can login. For security reasons, we do not automatically sign the user in after registering.

Once signed in, those links will change to show "My Account" links:

Header Account Links when Signed In

Here we see the link to the cart page.

Header Cart Icon

This link has several additional behaviors:

It shows how many items are currently in the cart

Header Cart Icon with Count

When an item is added, a cart summary slides over with the cart’s contents and a button to proceed to checkout.

Header Cart Icon expanded

And, if clicked, this will take the user to view the cart. See our Cart walkthrough.

Filling out the Body

Next, let’s examine the body of the homepage. Here we see:

Let’s examine the banner.

Banner

We derive this from a ContentTargeter in the Personalization Service.

Note

A ContentTargeter is a simple component for content management allowing different ContentItems to be served based on customizable rules.

ContentItems are just the content to be displayed, whether an asset, HTML, or plain text. They also come with display rules so that you could show one ContentItem to signed-in users while another to anonymous ones for the same targeter.

To retrieve its content, we send a request to the Personalization Services with the name of the targeter: Homepage Banner Ad. We have this setup to be configurable with a property HOME_HERO_CONTENT_TARGETER, so it’s easy to change without getting into the code. The request can also specify the selected locale and other attributes against which the ContentItems' rules may be evaluated.

Request

POST /api/personalization/content-targeters/item

With the Commerce SDK
import { useAuth } from '@broadleaf/auth-react';
import { ClientOptions } from '@broadleaf/commerce-core';
import { PersonalizationClient } from '@broadleaf/commerce-personalization';

async function getContentItem(options: ClientOptions) {
    const client = new PersonalizationClient(options);
    const { isAuthenticated, getAccessToken } = useAuth();
    const accessToken = isAuthenticated ? await getAccessToken() : undefined;
    const targeterContext = {
      "locale": "en-US",
      "attributes": {
        "webRequest": {
          "fullUrl": "http://localhost:4000/",
          "pathname": "/",
          "userAgentType": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 OPR/76.0.4017.123",
          "secure": false,
          "searchKeywords": ""
        }
      }
    };
    return client.getContentItem(process.env.NEXT_PUBLIC_HOME_HERO_CONTENT_TARGETER, {
      accessToken,
      targeterContext,
    });
}
Request Payload
{
  "name": "Homepage Banner Ad",
  "locale": "en-US",
  "attributes": {
    "webRequest": {
      "fullUrl": "http://localhost:4000/",
      "pathname": "/",
      "userAgentType": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36 OPR/76.0.4017.123",
      "secure": false,
      "searchKeywords": ""
    }
  }
}
Response Payload
{
  "id": "01F5NQM3C18N681CMTH3S90ZWG",
  "name": "Default Content Item",
  "activeStartDate": "2020-02-20T16:16:05.953Z",
  "priority": 2,
  "type": "IMAGE",
  "asset": {
    "tenantId": "5DF1363059675161A85F576D",
    "type": "IMAGE",
    "provider": "BROADLEAF",
    "url": "/default-hero.jpg",
    "contentUrl": "https://admin.blcdemo.com/api/asset/content/default-hero.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
    "tags": []
  }
}
Warning
Note that the actual digital asset for the banner is not returned from the Personalization Service. Instead we get a content URL along with some metadata like alt-text and title. We’ll use this content URL to resolve the asset: It will usually either point to Broadleaf’s content resolution endpoint in the Asset Services or to a CDN.

Top Selling Products

At the bottom of the main body of content on the homepage, we have a curated list of top-selling or featured products.

Top Selling Products

We’ve set this up as a Category that is manually curated. It is mapped to the homepage’s context path (i.e., /), but you can set it up to be mapped differently. To retrieve it and summaries of its products, we need to send a request to the category-details endpoint in the Catalog-Browse Service. In the request, we just include the URL the category is mapped to: /. Alternatively, we could include the exact ID of the category, if we know it before hand.

The request can take not only the URL or ID of the category we want, but also some additional parameters to restrict the included set of products. We can include the following paging params (or any others that you configure):

  • Size: How many results we want. Default for this section is 4.

  • Offset: Where in the entire result set to start from. Default is 0.

  • Forward: Whether we are paging forward or backward from the offset, i.e., whether to get 4 (size) products after (backward) at the 10th (offset) result or before (forward). Default is true.

The response will include the top-level category info like name and description and the top-level product info like name, price, URI. This will not fully hydrate the product hierarchies (like options and assets), but only return a summary of the product’s info since we don’t normally want to display all of those details on a category details or browse page. Furthermore, like with search requests we include the pricing headers.

Request

GET /api/catalog-browse/categories/details?categoryUrl=/

With the Commerce SDK
import { BrowseClient } from '@broadleaf/commerce-browse';
import { ClientOptions } from '@broadleaf/commerce-core';

async function getContentItem(options: ClientOptions) {
    const client = new BrowseClient(options);
    return client.getCategoryByUrl('/', {
      locale,
      priceContext,
    });
}
Price Context Header
{
  "locale": "en-US",
  "currency": "USD",
  "userTargets": [
    {
      "targetValue": "authUser.serviceId",
      "targetType": "CUSTOMER"
    }
  ],
  "attributes": {
    "webRequest": {
      "fullUrl": "https://www.heatclinic.com/",
      "pathname": "/",
      "userAgentType": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36 OPR/72.0.3815.320",
      "secure": true,
      "searchKeywords": ""
    }
  }
}
Price Info Context Header
{
  "priceLists": [
    {
      "id": "string",
      "name": "string",
      "type": "STANDARD|SALE|CONTRACT",
      "priority": 0,
      "currency": "USD|MXN|CAD|EUR|GBP|etc"
    }
  ],
  "skipDetails": true
}
Response Payload

Note that the content was reduced to 1 product to reduce size.

{
  "id": "category7",
  "products": {
    "content": [
      {
        "id": "product1",
        "sku": "HS-GG-20",
        "priceInfo": {
          "price": {
            "amount": 9.99,
            "currency": "USD"
          },
          "priceType": "salePrice",
          "priceListId": "hc_base_sales",
          "priceTypeDetails": {
            "salePrice": {
              "type": "salePrice",
              "bestPrice": {
                "amount": 9.99,
                "currency": "USD"
              },
              "priceListId": "hc_base_sales",
              "priceDetails": {}
            },
            "basePrice": {
              "type": "basePrice",
              "bestPrice": {
                "amount": 11.99,
                "currency": "USD"
              },
              "priceDetails": {}
            }
          }
        },
        "currency": "USD",
        "options": [],
        "variants": [],
        "includedProducts": [],
        "promotionalProducts": {},
        "primaryAsset": {
          "tenantId": "5DF1363059675161A85F576D",
          "type": "IMAGE",
          "provider": "BROADLEAF",
          "url": "/Green-Ghost-Bottle.jpg",
          "contentUrl": "https://{demo-admin-host}/api/asset/content/Green-Ghost-Bottle.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
          "altText": "Bottle of Green Ghost",
          "title": "Bottle of Green Ghost",
          "tags": [],
          "id": "product1_primary",
          "productId": "product1",
          "primary": true,
          "sorted": true,
          "parentId": "product1"
        },
        "inventoryType": "PHYSICAL",
        "activeStartDate": "2020-11-13T08:50:48.892360Z",
        "merchandisingProduct": false,
        "description": "Made with Naga Bhut Jolokia, the World's Hottest pepper.",
        "active": true,
        "individuallySold": true,
        "uri": "/hot-sauces/green-ghost",
        "searchable": true,
        "inventoryCheckStrategy": "NEVER",
        "assets": [
          {
            "tenantId": "5DF1363059675161A85F576D",
            "type": "IMAGE",
            "provider": "BROADLEAF",
            "url": "/Green-Ghost-Bottle.jpg",
            "contentUrl": "https://{demo-admin-host}/api/asset/content/Green-Ghost-Bottle.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
            "altText": "Bottle of Green Ghost",
            "title": "Bottle of Green Ghost",
            "tags": [],
            "id": "product1_primary",
            "productId": "product1",
            "primary": true,
            "sorted": true,
            "parentId": "product1"
          }
        ],
        "inventoryReservationStrategy": "NEVER",
        "fulfillmentFlatRates": {},
        "eligibleForPickup": false,
        "name": "Green Ghost",
        "online": true,
        "onSale": false,
        "attributes": {},
        "availableOnline": true,
        "discountable": true,
        "reviewsSummary": {
          "numberOfReviews": 0
        }
      }
    ],
    "last": false,
    "numberOfElements": 4,
    "size": 4,
    "pageable": {
      "paged": true,
      "forward": true,
      "offset": 0,
      "underlyingSize": 4,
      "pageSize": 4,
      "unpaged": false
    },
    "sort": {
      "sorted": false,
      "unsorted": true,
      "empty": true,
      "orders": []
    },
    "first": true,
    "empty": false
  },
  "displayTemplate": "DEFAULT",
  "activeStartDate": "2020-11-13T08:50:48.892360Z",
  "description": "The Heat Clinic's Top Selling Sauces",
  "externalId": "topSellersExternalId",
  "taxCode": "US",
  "metaDescription": "The Heat Clinic's Top Selling Sauces",
  "url": "/",
  "showInSiteMap": true,
  "productMembershipType": "EXPLICIT",
  "metaTitle": "Top Sellers",
  "name": "Top Sellers",
  "promotionalProducts": {},
  "attributes": {},
  "breadcrumbs": [
    {
      "label": "Top Sellers"
    }
  ]
}

Finally, let’s examine the footer. We see:

Footer

Let’s take a look at the footer navigation menu.

Footer Menu

We maintain this menu in the Admin, and derive it from the Menu Service, just like with the main navigation in the header. To retrieve it, we just need its name: FOOTER_MENU. We have this setup to be configurable with a property FOOTER_MENU_NAME, so it’s easy to change without getting into the code.

This menu is setup with 3 submenus to demonstrate how we can use a single call to retrieve a menu-hierarchy. See the main navigation section for more details on Menu and MenuItem.

Request

GET /api/menu/menu-hierarchy?menuName=FOOTER_MENU

With the Commerce SDK
import { ClientOptions } from '@broadleaf/commerce-core';
import { MenuClient } from '@broadleaf/commerce-menu';

async function getContentItem(options: ClientOptions) {
    const client = new MenuClient(options);
    return client.getMenuHierarchy(process.env.NEXT_PUBLIC_FOOTER_MENU_NAME);
}
Response Payload
{
  "id": "01E0016QJVTZRB08JHS8GF1H3N",
  "name": "FOOTER_MENU",
  "submenu": [
    {
      "id": "01E001B54K7E7Y0SEJW8XG1NHK",
      "parentMenuId": "01E0016QJVTZRB08JHS8GF1H3N",
      "label": "About Us",
      "displayOrder": 1000,
      "type": "TEXT",
      "submenu": [
        {
          "id": "01E001JD943P3Z1CWZEX331M7M",
          "parentMenuItemId": "01E001B54K7E7Y0SEJW8XG1NHK",
          "parentMenuId": "01E0016QJVTZRB08JHS8GF1H3N",
          "label": "Company Info",
          "url": "/company-info",
          "displayOrder": 1000,
          "type": "LINK",
          "submenu": []
        },
        {
          "id": "01E001SW9B70RW0W3GC8AA1FVT",
          "parentMenuItemId": "01E001B54K7E7Y0SEJW8XG1NHK",
          "parentMenuId": "01E0016QJVTZRB08JHS8GF1H3N",
          "label": "Contact Us",
          "url": "/contact-us",
          "displayOrder": 2000,
          "type": "LINK",
          "submenu": []
        },
        {
          "id": "01E001VSEBJ6W311X5HRFE0DQB",
          "parentMenuItemId": "01E001B54K7E7Y0SEJW8XG1NHK",
          "parentMenuId": "01E0016QJVTZRB08JHS8GF1H3N",
          "label": "Privacy Policy",
          "url": "/privacy",
          "displayOrder": 3000,
          "type": "LINK",
          "submenu": []
        },
        {
          "id": "01E001XC8FTZQQ13BDE2Y71S3C",
          "parentMenuItemId": "01E001B54K7E7Y0SEJW8XG1NHK",
          "parentMenuId": "01E0016QJVTZRB08JHS8GF1H3N",
          "label": "Terms of Use & Terms of Sale",
          "url": "/terms",
          "displayOrder": 4000,
          "type": "LINK",
          "submenu": []
        },
        {
          "id": "01E001YZRBMSEN0HQ8JHQZ0WD9",
          "parentMenuItemId": "01E001B54K7E7Y0SEJW8XG1NHK",
          "parentMenuId": "01E0016QJVTZRB08JHS8GF1H3N",
          "label": "CA Supply Chain Act",
          "url": "/ca-supply-chain-act",
          "displayOrder": 5000,
          "type": "LINK",
          "submenu": []
        }
      ]
    }
  ]
}

Next let’s look at the social links.

Footer Social Links

We’ve set this up as static text in the frontend itself, but this could be setup using a managed Menu or made a sub-menu of the main footer menu.

Finally, we have the site’s copyright.

Site Copyright

This is static text in the frontend, but this could be setup as a managed ContentTargeter.