Build Static Sites with Salesforce CMS Headless APIs and Heroku

Salesforce CMS opens up multiple ways and channels for you to surface all the varieties of your authored content — be it marketing materials, news articles or blog posts. One such quick and easy way is to use our Delivery APIs in a Spring Boot web application deployed on Heroku. Read more to find out how this can be done in few simple steps.

Salesforce CMS and headless content delivery

With content authored in Salesforce CMS, you can leverage the powerful Experience Builder to spin up rich and beautiful experiences for your customers. But what if you want to re-use the same content on a web-app or static site that you’ve hosted on a separate cloud platform?

We’ve got you covered. Salesforce CMS is a hybrid CMS, meaning your teams can create content in a central location and syndicate it to any digital touchpoint, whether it’s an experience powered by Salesforce or another system. Which means that with the help of the Headless Content Delivery APIs, content created once can be re-used across multiple channels. These can be anything ranging from Marketing Cloud, LEX apps to cloud platforms like Heroku or AWS Elastic Beanstalk to name a few!

In this blog, we’re going to see how to quickly and easily create a simple Spring Boot web application that re-uses content from Salesforce CMS using our headless APIs, and also get it up and running on Heroku.

Prerequisites:

  • A Salesforce account with the CMS App enabled
  • A Heroku account
  • ‘Customize Application’ or ‘Modify All Data’ user permissions (to create Content Type)

Creating content in Salesforce CMS

Creating your blog content type

Content type defines the structure of content you create. These content types are global and available across workspaces. You can create content based on these types after they’re created. Until now, we’ve had to go and make an explicit POST call the Tooling API endpoint with a JSON payload to get a custom content type created.

Well, not anymore! Thanks to the lovely folks at Salesforce Labs, we now have an awesome AppExchange package called CMS Content Type Creator that makes building out your Content Type a breeze! So let’s get your blog content type created.

Authoring content

Follow this blog post to create content in a workspace, add channel, contributors and translation.

  • Note: Since we want the images we use in blog articles to be available to public users on our deployed Heroku app, make sure to make the image asset files accessible to unauthenticated users. You can do this by going to Setup > Asset Files > Edit and enable the “Let unauthenticated users see this asset file” setting for your image asset files.

Configure a channel for your CMS Workspace

The content we’ve authored and published can be accessed using our headless APIs by configuring a channel for your CMS Workspaces. Channels are basically the end-points where you want your content to be shared to. In our case, we want to syndicate our content to a standalone Heroku app, so we’ll go ahead and create one using a permission set.

  • Create a permission set for your Heroku app…

… and assign it to the user you want to be using in your Heroku app.
Click to add an image

 

  • From the Salesforce CMS app, navigate to your CMS Workspace and create a new channel using the created permission set.

 

  • Channel created! Hang on. We’re almost done with the setup now…

  • The only thing remaining now is to take a note of the channelId for the channel we just created. We’ll need this to access our Content Delivery APIs below.

To get the Channel ID, we can use the Managed Content Delivery Channels API

Salesforce CMS Content Delivery APIs

The Content Delivery API enables you to retrieve content from Salesforce CMS and use it to build and deliver custom experiences. You can use the API to help build components in Lightning as well as reuse content from Salesforce CMS in Visualforce pages.
For our case, we’ll be using the API in a completely headless fashion, enabling our lightweight web app to use the Salesforce CMS content directly.

Prerequisites:

API

The Content Delivery API is read-only with the following signature.

EndPoint

URL

GET:/connect/cms/delivery/channels/{:channelId}/contents/query

URL parameters

Param Name (New Only) Required? Values Description Since
managedContentType YES String The developer name of the managed content type. 48
managedContentIds NO List<String> List of Managed content ids with the max limit as 200 48
page NO int An integer specifying a page of items. The default value is 0, which returns the first page 48
pageSize NO int Specifies the number of items per page. Valid values are from 1 through 250. The default value is 25 48
language NO String Specifies the language of the content. 48
includeMetadata NO Boolean Specifies whether to include metadata in the response (true) or not (false). The default value is false. 48
startDate NO String Publish start date in ISO 8601 format, for example, 2011-02-25T18:24:31.000Z. 48
endDate NO String Publish end date in ISO 8601 format, for example, 2011-02-25T18:24:31.000Z 48

You can find more details about the API here.
Note: The Content Delivery EndPoint is available from v48.0

Create your Spring Boot application

Spring Boot is a great framework to get a web application up and running fast! Here’s a quick tutorial to get you started if needed.

In addition to being light, fast and developer friendly, Spring Boot is also a great framework to be used with Heroku to deploy with ease.

Let’s get started on building out our application.

  1. Create your SpringBootApplication class

SalesforceCmsBlogApplication.java

package com.salesforce.cmsdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;


@SpringBootApplication
public class SalesforceCmsBlogApplication implements WebMvcConfigurer {
    public static void main(String[] args) {
        SpringApplication.run(BlogSpringBootApplication.class, args);
    }
}

 

  1. Add your Salesforce configuration parameters in the application.properties file.
sfdc.cms.hostname=<Your Salesforce instance URL>
sfdc.cms.username=<Username>
sfdc.cms.password=<Password>
sfdc.cms.clientid=<Consumer Key for the configured Connected App>
sfdc.cms.clientsecret=<Consumer Secret for the configured Connected App>
sfdc.cms.channelid=<Channel Id for the Content Delivery API>
sfdc.cms.redirecturi=<Oauth Login Redirect URI (optional)>

We’ll be reading these properties into member variables in our Autowired service through the @Value annotation and using them to establish API access with our Salesforce org.

  1. Define your Blog POJO to encapsulate the CMS Content Type data that you need for your web application.

Blog.java

package com.salesforce.cms.blog;

public class Blog {

    String title;
    String body;
    String headlineImage;
    String date;
    String authorName;
    
    public Blog(String title, String body, String headlineImageUrl, String date, String authorName) {
        super();
        this.title = title;
        this.body = body;
        this.headlineImageUrl = headlineImageUrl;
        this.date = date;
        this.authorName = authorName;
    }
    
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getBody() {
        return body;
    }
    public void setBody(String body) {
        this.body = body;
    }
    public String getHeadlineImage() {
        return headlineImage;
    }
    public void setHeadlineImage(String headlineImage) {
        this.headlineImage = headlineImage;
    }
    public String getDate() {
        return date;
    }
    public void setDate(String date) {
        this.date = date;
    }
    public String getAuthorName() {
        return authorName;
    }
    public void setAuthorName(String authorName) {
        this.authorName = authorName;
    }
    
    
}

This is basically a simple encapsulation of the content item that we created in Salesforce CMS. We’ll be binding data in this POJO to our model for rendering in the UI.

  1. Create the Java service class that will invoke the Salesforce CMS Delivery API to fetch and parse the blog.
    • Get the API Access Token
    private String getAccessToken() { 
        Client client = ClientBuilder.newClient();
        
        Form formData = new Form();
        formData.param("grant_type", "password");
        formData.param("client_id",clientId);
        formData.param("client_secret", clientSecret);
        formData.param("format","json");
        formData.param("redirect_uri", oAuthRedirectUri);
        formData.param("username", username);
        formData.param("password", password);
                
        Response response = client
                .target(hostName)
                .path("/services/oauth2/token")
                .request(MediaType.APPLICATION_JSON)
                .post(Entity.form(formData));
        
        String tokenResponse = response.readEntity(String.class);
        
        String accessToken = null;
        try {
            JSONObject jsonResp = (JSONObject) new JSONParser().parse(tokenResponse);
            accessToken = (String) jsonResp.get("access_token");
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
        return accessToken;
    }
    • Invoke the Salesforce CMS Content Delivery API to fetch the list of “blog” type articles
public List<Blog> getBlogs() {
        Client client = ClientBuilder.newClient();
        
        Response response = client
                .target(hostName)
                .path("/services/data/v48.0/connect/cms/delivery/channels/" + channelId + "/contents/query")
                .queryParam("managedContentType", "Blogs")
                .request("application/json")
                .header("Authorization", "OAuth " + getAccessToken())
                .header("X-PrettyPrint", "1")
                .get();
        
        String queryResponse = response.readEntity(String.class);
        
        List<Blog> blogContentList = parseBlogItems(queryResponse);
   
        return blogContentList;
    }

This is how CmsContentDeliveryService looks in all its “Springy“ glory:

package com.salesforce.cms.blog;

import java.util.List;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class CmsContentDeliveryService {
    
    @Value("${sfdc.cms.channelid}")
    String channelId;
    
    @Value("${sfdc.cms.hostname}")
    String hostName;
    
    @Value("${sfdc.cms.clientid}")
    String clientId;
    
    @Value("${sfdc.cms.clientsecret}")
    String clientSecret;
    
    @Value("${sfdc.cms.username}")
    String username;
    
    @Value("${sfdc.cms.password}")
    String password;
    
    @Value("${sfdc.cms.redirecturi}")
    String oAuthRedirectUri;
    
    public List<Blog> getBlogs() {
        Client client = ClientBuilder.newClient();
        
        Response response = client
                .target(hostName)
                .path("/services/data/v48.0/connect/cms/delivery/channels/" + channelId + "/contents/query")
                .queryParam("managedContentType", "blogs")
                .request("application/json")
                .header("Authorization", "OAuth " + getAccessToken())
                .header("X-PrettyPrint", "1")
                .get();
        
        String queryResponse = response.readEntity(String.class);
        
        List<Blog> blogContentList = parseBlogItems(queryResponse);
   
        return blogContentList;
    }
    
   private List<Blog> parseBlogItems(String queryResponse) {
        List<Blog> parsedBlogItems = new ArrayList<>();
        
        try {
            JSONObject responseJson = (JSONObject) new JSONParser().parse(queryResponse);
            
            // Get the content "items" array 
            JSONArray blogItems = (JSONArray) responseJson.get("items");
            for(int i=0; i<blogItems.size(); i++) {
                // Parse a single item and add to the Blog object list
                JSONObject blogItemJson = (JSONObject) blogItems.get(i);
                JSONObject blogContentNodesJson =(JSONObject) blogItemJson.get("contentNodes");    
                Blog blog = getBlogFromContentNodes(blogContentNodesJson);
                
                parsedBlogItems.add(blog);
            }
        } catch (ParseException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return parsedBlogItems;
    }
    
    private Blog getBlogFromContentNodes(JSONObject contentNodesJson) {
        String title = (String) ((JSONObject)contentNodesJson.get("BlogTitle")).get("value");
        String author = (String) ((JSONObject)contentNodesJson.get("AuthorName")).get("value");
        String body = (String) ((JSONObject)contentNodesJson.get("BlogBody")).get("value");
        String date = (String) ((JSONObject)contentNodesJson.get("AuthorDate")).get("value");
        String imageUrl = hostName + (String) ((JSONObject)contentNodesJson.get("BlogImageMain")).get("unauthenticatedUrl");
        
        return new Blog(title, body, imageUrl, date, author);
    }

    private String getAccessToken() { 
        Client client = ClientBuilder.newClient();
        
        Form formData = new Form();
        formData.param("grant_type", "password");
        formData.param("client_id",clientId);
        formData.param("client_secret", clientSecret);
        formData.param("format","json");
        formData.param("redirect_uri", oAuthRedirectUri);
        formData.param("username", username);
        formData.param("password", password);
                
        Response response = client
                .target(hostName)
                .path("/services/oauth2/token")
                .request(MediaType.APPLICATION_JSON)
                .post(Entity.form(formData));
        
        String tokenResponse = response.readEntity(String.class);
        
        String accessToken = null;
        try {
            JSONObject jsonResp = (JSONObject) new JSONParser().parse(tokenResponse);
            accessToken = (String) jsonResp.get("access_token");
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
        return accessToken;
    }

}

 

  1. Write your Controller class to map incoming GET requests…

BlogHomeController.java

package com.salesforce.cms.blog;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class BlogHomeController {
    
    @Autowired
    CmsContentDeliveryService contentService;
    
    @GetMapping("/")
    public String homepage(Model model) {
        List<Blog> blogs = contentService.getBlogs();
        model.addAttribute("blogs", blogs);
        
        return "index";
    }
    
}

As you can see, once we get the blogs data from our contentService, we add that attribute to the model which will then be rendered by the Thymeleaf template engine using the HTML template that we specify.

  1. And finally, define the HTML template to display the articles…

index.html

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head lang="en">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Food behind the cloud</title> 
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet"
    href="https://fonts.googleapis.com/css?family=Karma">
</head>
<style>
body,h1,h2,h3,h4,h5,h6 {font-family: "Karma", sans-serif}
.w3-bar-block .w3-bar-item {padding:20px}
</style>
<body>

    <!-- Top menu -->
    <div class="w3-top">
        <div class="w3-indigo w3-large"
            style="margin: auto">
            <div class="w3-center w3-padding-16"> ☁️ Food behind the cloud ☁️ </div>
        </div>
    </div>
    
    <div class="w3-main w3-content w3-padding" style="max-width:1200px;margin-top:100px">

    <!-- First Photo Grid-->
    <div class="w3-row-padding w3-padding-16 w3-center" id="food">
        <div th:each="blog : ${blogs}" class="w3-quarter">
            <img th:src="${blog.headlineImageUrl}" alt="${blog.headlineImageUrl}"
                 width="272" height="408">
            <h3 th:text="${blog.title}">Article Title</h3>
            <p th:utext="${blog.body}">Article Body</p>
        </div>
    </div>

    <!-- Pagination -->
    <div class="w3-center w3-padding-32">
        <div class="w3-bar">
            <a href="#" class="w3-bar-item w3-button w3-hover-black">«</a> <a
                href="#" class="w3-bar-item w3-black w3-button">1</a> <a href="#"
                class="w3-bar-item w3-button w3-hover-black">2</a> <a href="#"
                class="w3-bar-item w3-button w3-hover-black">3</a> <a href="#"
                class="w3-bar-item w3-button w3-hover-black">4</a> <a href="#"
                class="w3-bar-item w3-button w3-hover-black">»</a>
        </div>
    </div>

    <hr>
    </div>
    <!-- End page content -->
</body>
</html>

We’ve not covered the styling information here as its not strictly of relevance to this blog post. For more details and the code, please check the link to the repository.

Deploying to Heroku

  • Login to your Heroku account through the heroku CLI
$ heroku login
  • Create a new Heroku app
$ heroku create
  • Push the Spring Boot application to Heroku
$ git push heroku master

 

Example Code Repo

Summary

This post has shown one of the many ways in which content created in Salesforce CMS can be surfaced in an external app deployed on a platform like Heroku using our Delivery APIs.

The headless nature of Salesforce CMS content makes it flexible to author once, and consume anywhere that meets your needs.

Resources

Previous posts in this series

About the author

Ishan Shrivastava
Ishan is SMTS at Salesforce and is based out of Hyderabad, India. You can connect with Ishan at LinkedIn.