Using Apex to Get CMS Product Thumbnail Images For Lightning Web Components In Salesforce B2BCommerce

Brett Dworaczyk
6 min readJun 4, 2024

--

Salesforce Logo (Blue Cloud on White Background)

UPDATE: I have also published a different article that demonstrates a way to pull product list images along with product metadata without using CMS endpoints here.

The Salesforce ConnectApi namespace documentation provides a comprehensive list of supported classes, including descriptions and additional details such as the available class methods. Despite the existence of this documentation, engineers may find it challenging to discern the appropriate hierarchy and sequence of operations required for specific behaviors. There are often multiple ways to utilize this API to manipulate the data being retrieved, but the main difficulty lies in identifying the most efficient method.

A pertinent example is retrieving CMS image content for use with LWCs (Lightning Web Components) in Experience Builder for B2B Commerce sites. The complexity and potential for frustration are significant, primarily due to the unclear order of operations and internal hierarchy when dealing with content channels, delivery channels, collections, workspaces, product media, related components, etc. After thoroughly examining the documentation and reviewing numerous classes and methods to develop my custom component, I am outlining my methodology for achieving this task in the most efficient manner possible, based on my understanding from the documentation. In other words, this represents the shortest path to the solution. It’s actually a simple process.

Use Case

To develop my custom LWC, I needed to create a Featured Products component that includes product thumbnail images, storefront links, and product names, with the capability to easily extend this to retrieve additional product data if necessary. To handle the majority of the processing using Apex code, I leveraged the classes and methods I determined to be the most essential from the ConnectApi namespace for this purpose, and this helped to greatly simplify the process. These included the following:

  • ConnectApi.ManagedContentdocumentation for this class
  • ConnectApi.ManagedContentVersionCollection — output class returned from the ConnectApi.ManagedContent.getManagedContentByIds() static method

These were used in conjunction with the following Salesforce sObjects:

  • Product2 — stores product record data
  • ProductMedia — stores metadata for media, images, and attachments that have been added to products
  • Network — stores record data for Experience Cloud sites

As you can see from the above listings, not much was actually required in order to accomplish the task. Next, let’s look at the Apex code that achieved the required results.

Apex Code

Create A Wrapper Class To Store Related Data

I needed to store more than just Product2 object record data (in this case the product thumbnail image URL from the CMS), and as object record data isn’t extensible, I needed a solution that allowed additional data fields to be included in the same return payload. The obvious solution to this problem was a simple wrapper class:

// the wrapper class for featured product data
public class featuredProductWrapper {
@AuraEnabled
public Product2 featuredProduct { get; set; }
@AuraEnabled
public ConnectApi.ManagedContentVersionCollection productImage { get; set; }

// the class assignments are handled in the constructor
public featuredProductWrapper(
Product2 featuredProduct,
ConnectApi.ManagedContentVersionCollection productImage
) {
this.featuredProduct = featuredProduct;
this.productImage = productImage;
}
}

As we plan on having multiple products returned, we’ll need to create a list to store the data for the return payload. This wrapper class will be used for this purpose, and can be instantiated using the following code:

List<featuredProductWrapper> productData = new List<featuredProductWrapper>();

This wrapper class will be used after the steps that follow. All of the Apex code is presented at the end of this article.

Get Product2 Object Records

Our instance of Salesforce uses an important custom field on the Product2 object, named “Featured Product” (Featured_Product__c). This custom field is simply a checkbox (boolean) field that can be checked on any number of products to mark them as featured products. These are the products getting pulled via Apex, using SOQL, for use with the LWC:

// get all featured products that are currently active products
List<Product2> featuredProducts = [
SELECT Id, Name
FROM Product2
WHERE IsActive = TRUE AND Featured_Product__c = TRUE
];

We’ll also need the Community Id, which is a short and simple query on the Network object:

// get the Community Id using the Name field
Id communityId = [
SELECT Id
FROM Network
WHERE Name = 'Network Name Here'
LIMIT 1].Id;

The next steps involve commands executed during the iteration of the Product2 object record list featuredProducts. These steps involve the following:

  1. Get the associated ProductMedia object record, which is the record that contains the media, images, and attachments that have been added to the product
    NOTE: The important field here is the ElectronicMediaId field. Despite the different name, this field value IS THE SAME VALUE as the ManagedContent id. This is where the association resides.
  2. Create a List of type String to hold the content id, as content ids always need to be passed in a list List<String>…, and add the content id to this list
  3. Pull the image data (which includes the thumbnail URL I needed) using the content id by calling the getManagedContentByIds() static method of the ManagedContent class
// iterate over the Product2 object record list
for (Product2 featuredProduct : featuredProducts) {
// get the Product2 object record id
Id productId = featuredProduct.Id;

// get the ProductMedia object record that corresponds
// to the attached Product2 object record
ProductMedia productMediaMetadata = [
SELECT Id, ElectronicMediaId
FROM ProductMedia
WHERE ProductId = :productId
LIMIT 1
];

/*
The ElectronicMediaId field value of the ProductMedia object
is the managed content id
*/

// get the ElectronicMediaId value from the ProductMedia record
Id mediaId = productMediaMetadata.ElectronicMediaId;

// the managed content ids have to be in a List of type String
List<String> contentIds = new List<String>();
contentIds.add(mediaId);

// get the thumbnail image data
ConnectApi.ManagedContentVersionCollection imageData = ConnectApi.ManagedContent.getManagedContentByIds(
communityId,
contentIds,
0,
1,
'en_US',
'cms_image'
);

// add all of the product data to the wrapper class list
productData.add(new featuredProductWrapper(featuredProduct, imageData));

Summary

Although the ConnectApi documentation isn’t clear, pulling product thumbnail images is an easy process via Apex and SOQL. With just a handful of lines of code, this data can be pulled and returned for any product that has associated ProductMedia object records. The full code for both classes is below:

/**
* @description : Gets the active featured products, along with their associated thumbnail images
* @author : Brett Dworaczyk
* @date : 06/03/2024
*/

public with sharing class B2BFeaturedProducts {

public B2BFeaturedProducts() {}

// get the featured products and associated thumbnail images
@AuraEnabled(cacheable=true)
public static List<featuredProductWrapper> getFeaturedProducts() {

// initialize a list of the wrapper class type for storing return data
List<featuredProductWrapper> productData = new List<featuredProductWrapper>();

// get all of the featured products that are currently active
List<Product2> featuredProducts = [
SELECT Id, Name
FROM Product2
WHERE IsActive = TRUE AND Featured_Product__c = TRUE
];

// get the id of the relevant community
Id communityId = [
SELECT Id
FROM Network
WHERE Name = 'Network Name Here'
LIMIT 1].Id;

// get the ProductMedia records associated with each product
for (Product2 featuredProduct : featuredProducts) {
Id productId = featuredProduct.Id;
ProductMedia productMediaMetadata = [
SELECT Id, ElectronicMediaId
FROM ProductMedia
WHERE ProductId = :productId
LIMIT 1
];

/*
The ElectronicMediaId field value of the ProductMedia object is
the managed content id
*/

// get the ElectronicMediaId value from the ProductMedia record
Id mediaId = productMediaMetadata.ElectronicMediaId;

// the managed content ids have to be in a list of type string
List<String> contentIds = new List<String>();
contentIds.add(mediaId);

// get the thumbnail image data
ConnectApi.ManagedContentVersionCollection imageData = ConnectApi.ManagedContent.getManagedContentByIds(
communityId,
contentIds,
0,
1,
'en_US',
'cms_image'
);

productData.add(new featuredProductWrapper(featuredProduct, imageData));
}

return productData;
}

// the wrapper class for featured product data
public class featuredProductWrapper {
@AuraEnabled
public Product2 featuredProduct { get; set; }
@AuraEnabled
public ConnectApi.ManagedContentVersionCollection productImage { get; set; }

public featuredProductWrapper(
Product2 featuredProduct,
ConnectApi.ManagedContentVersionCollection productImage
) {
this.featuredProduct = featuredProduct;
this.productImage = productImage;
}
}
}

If your use case involves additional fields, objects, or records, you can easily expand the wrapper class to support any additional SOQL records and/or data types necessary.

--

--