Build Extensions for Salesforce CMS
Display Salesforce CMS Content
Display CMS Content in Experience Builder Sites
Build extensions for Salesforce CMS to boost productivity for content authors, enabling them to draft, revise, or customize their content with external tools right in the editor.
You can build extensions that work with any third-party tool. This topic explains how to build an extension that integrates with the TLDR API, an external AI service, that provides a summary of existing content. This process involves developing an Apex controller with a secure callout and a Lightning web component (LWC) that uses specific metadata to target the CMS content editor UI.
You can connect this type of extension to any third-party generative AI API, such as Gemini or ChatGPT.
For this extension, we use named credentials for secure external API callouts to make sure that your API key isn’t exposed in code. The name of the credential in the Apex controller is tldrthis. For information and instructions about configuring a named credential, see Named Credentials in the Apex Developer Guide.
Create an Apex Controller to securely connect the extension to the TLDR API. Set up the callout by using the named credential. The callout abstracts the API interaction to ensure security. See Apex Server-Side Controller Overview in the Lightning Aura Components Developer Guide.
1public class TldrSummaryExtensionController {
2
3 @AuraEnabled(cacheable=true)
4 public static String getSummary(String summaryInputText, Integer minLength,
5 Integer maxLength,String apiType) {
6 String urlToInvoke = 'AI'.equals(apiType)?
7 'v1/model/abstractive/summarize-text/':
8 'v1/model/extractive/summarize-text/';
9
10 HttpRequest req = new HttpRequest();
11 // Use 'callout:aiContentService' to reference the Named Credential
12 req.setEndpoint('callout:tldrthis/'+urlToInvoke);
13 req.setHeader('content-type', 'application/json');
14 req.setHeader('x-rapidapi-host', 'tldrthis.p.rapidapi.com');
15 req.setHeader('x-rapidapi-key', '{!$Credential.Password}');
16 // Get your key from TLDR API site
17 req.setMethod('POST');
18 // Construct the request body using the prompt and context
19 JSONGenerator requestBody = JSON.createGenerator(true);
20 requestBody.writeStartObject();
21 requestBody.writeStringField('text', summaryInputText);
22 requestBody.writeNumberField('min_length', minLength);
23 requestBody.writeNumberField('max_length', maxLength);
24 requestBody.writeStringField('type', contentType);
25 requestBody.writeEndObject();
26 req.setBody(requestBody.getAsString());
27
28 Http http = new Http();
29 HTTPResponse res = http.send(req);
30
31 return res.getBody();
32 }
33}To make your extension visible in the CMS content editor, set targets and targetConfigs in the extension’s configuration or .js-meta.xml file. The primary target value is lightning__CmsEditorExtension, which makes the extension appear in the extensions menu in the editor.
Set the targetConfig targets to lightning__CmsEditorExtension and set the height and width attributes for the extension’s floating panel. This table shows possible values for the height and width attributes.
| Attribute | Type | Description | Default |
|---|---|---|---|
| width | enum | Enter small, medium, large, or x-large. The semantic values correspond to these pixel values. small = 240 px medium = 320 px large = 400 px x-large = 640 px | medium |
| height | number | Enter a value between 200-600 px. | 400 px |
An extension with this targeting configuration is visible to all CMS content types, including marketing content types that support extensions.
This example shows the configuration file of a TLDR summary extension that’s available to all CMS and marketing content types that support extensions. When opened, the extension appears in a 400x400 px floating panel.
1<?xml version="1.0" encoding="UTF-8"?>
2<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3 <!-- The apiVersion may need to be updated for the current release -->
4 <apiVersion>66.0</apiVersion>
5 <isExposed>true</isExposed>
6 <masterLabel>TLDR Summary Extension</masterLabel>
7 <targets>
8 <target>lightning__CmsEditorExtension</target>
9 </targets>
10 <targetConfigs>
11 <targetConfig targets="lightning__CmsEditorExtension">
12 <size height="400" width="large"></size>
13 </targetConfig>
14 </targetConfigs>
15</LightningComponentBundle>To make an extension available only to specific content types, such as News or Document, specify the contentTypes under targetconfig for lightning__CmsEditorExtension. Set the contentType fullyQualifiedName to the fully qualified name (FQN) of the content type that you want to target. To target multiple content types, list multiple content types under targetconfig. You can target both marketing and non-marketing content types.
CMS Content Fully Qualified Names
| Content Type | Fully Qualified Name (FQN) |
|---|---|
| Audio | sfdc_cms__audio |
| Document | sfdc_cms__document |
| Image | sfdc_cms__image |
| News | sfdc_cms__news |
| Video | sfdc_cms__video |
This example shows the configuration file of an extension that’s available only to the News content type. When opened, the extension appears in a 400x400 px floating panel.
1<?xml version="1.0" encoding="UTF-8"?>
2<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3 <!-- The apiVersion may need to be updated for the current release -->
4 <apiVersion>66.0</apiVersion>
5 <isExposed>true</isExposed>
6 <masterLabel>TLDR Summary Extension</masterLabel>
7 <targets>
8 <target>lightning__CmsEditorExtension</target>
9 </targets>
10 <targetConfigs>
11 <targetConfig targets="lightning__CmsEditorExtension">
12 <size height="400" width="large"></size>
13 <contentTypes>
14 <contentType fullyQualifiedName="sfdc_cms__news"></contentType>
15 </contentTypes>
16 </targetConfig>
17 </targetConfigs>
18</LightningComponentBundle>The HTML file uses standard LWC markup to display the extension’s user interface in the floating panel. This UI is where the content author interacts with the extension.
In this example, the TLDR extension UI includes fields and buttons for selecting what part of the current content to summarize, setting the length of the summary, and generating the summary.
1<template>
2 <div>
3 <div>
4 <div>
5 <lightning-card variant="large" icon-name="utility:summary">
6 <h6 slot="title"><b> TLDR this</b></h6>
7 </lightning-card>
8 </div>
9 <h6><b>Create a summary for this Content Item</b></h6>
10 <!-- Min length used as input param in TLDR Api -->
11 <lightning-input
12 type="number"
13 label="Min Length"
14 max="300"
15 min="10"
16 placeholder="Enter Minimum length of the output summary"
17 onchange="{handleMinLengthChange}"
18 >
19 </lightning-input>
20 <lightning-input
21 type="number"
22 label="Max Length"
23 max="300"
24 min="10"
25 placeholder="Enter max length of the output summary"
26 onchange="{handleMaxLengthChange}"
27 >
28 </lightning-input>
29 <!-- Choose source field from content to pick up input data -->
30 <lightning-combobox
31 name="source"
32 label="Choose source"
33 value="{dafInputField}"
34 options="{contentOptions}"
35 onchange="{handleDafInputFieldChange}"
36 required
37 >
38 </lightning-combobox>
39 <!-- Choose target field from content to populate output data -->
40 <lightning-combobox
41 name="target"
42 label="Choose target"
43 value="{dafOutputField}"
44 options="{contentOptions}"
45 onchange="{handleDafOutputFieldChange}"
46 required
47 >
48 </lightning-combobox>
49 <!-- Choose TLDR summary API you want to use -->
50 <lightning-combobox
51 name="summary"
52 label="Choose Summarization Type"
53 value="{summaryAPIToCall}"
54 placeholder="Select Summary Type"
55 options="{summaryOptions}"
56 onchange="{handleSummaryChange}"
57 required
58 ></lightning-combobox>
59 <br />
60 <lightning-button
61 variant="brand"
62 label="Create summary"
63 title="Primary action"
64 onclick="{createSummary}"
65 ></lightning-button>
66 </div>
67 <br />
68 </div>
69</template>The JavaScript file defines the business logic of the extension and event handling. It uses the experience/cmsEditorApi methods to read and write content. The JavaScript file also manages the integration with the Apex controller, which, in this case, sends the existing text to the TLDR API and gets a summary.
This example shows the core logic of the TLDR extension, which gets content from the CMS content editor, creates a summary, and updates the content.
1import { LightningElement, api, wire } from "lwc";
2import { getContext, getContent, updateContent } from "experience/cmsEditorApi";
3
4import getSummary from "@salesforce/apex/TldrSummaryExtensionController.getSummary";
5
6/**
7 * This extension to get summary text using TLDR summary API.
8 * Please refer https://rapidapi.com/tldrthishq-tldrthishq-default/api/tldrthis used in this example
9 */
10export default class TldrSummary extends LightningElement {
11 // Get the content data through wire adapter using getContent API that will help in selecting the input and output field
12 @wire(getContent, {})
13 onContent(data) {
14 this.content = data;
15 }
16
17 //Get context details to get schema details of current content type
18 @wire(getContext)
19 context;
20
21 //Default paramter values for TLDR summary API
22 minLength = 10;
23 maxLength = 300;
24 summaryAPIToCall = "AI";
25
26 //Source field to pick up data to be summarized
27 dafInputField = "";
28
29 //Target field to populate using API
30 dafOutputField = "";
31
32 //TLDR Summary Type API value set on selection
33 handleSummaryChange(event) {
34 this.summaryAPIToCall = event.detail.value;
35 }
36
37 get summaryOptions() {
38 return [
39 {
40 label: "AI (Human-like)",
41 value: "AI",
42 },
43 {
44 label: "Key sentences",
45 value: "KEY_SENTENCES",
46 },
47 ];
48 }
49
50 handleDafOutputFieldChange(event) {
51 this.dafOutputField = event.detail.value;
52 }
53 handleDafInputFieldChange(event) {
54 this.dafInputField = event.detail.value;
55 }
56
57 // Selection box for current content Fields
58 get contentOptions() {
59 return this.getCurrentSchemaList();
60 }
61
62 // Get schema details or fields of current content selected
63 getCurrentSchemaList() {
64 let currentContentSchemaList = [];
65 const schema = this.context.data.schema.schema.properties;
66 for (const property in schema) {
67 if (this._isTextType(schema[property].$ref)) {
68 currentContentSchemaList.push({ label: schema[property].title, value: property });
69 }
70 }
71 return currentContentSchemaList;
72 }
73
74 handleMinLengthChange(event) {
75 this.minLength = event.detail.value;
76 }
77
78 handleMaxLengthChange(event) {
79 this.maxLength = event.detail.value;
80 }
81 /**
82 *
83 * Create a summary text by calling tldrthis api
84 */
85 async createSummary() {
86 if (typeof this.content.data.contentBody[this.dafOutputField] == "undefined") {
87 this.generateSummarizeText();
88 } else {
89 if (
90 confirm("Summary Target field already has some text in it. Would you like to overwrite it?")
91 ) {
92 this.generateSummarizeText();
93 } else {
94 console.log("Target data not modified");
95 }
96 }
97 }
98
99 // Method to pick up Source data -> Hit API and extract response -> Populate the target field
100 async generateSummarizeText() {
101 var sourceHtmlInput = this.content.data.contentBody[this.dafInputField];
102 if (sourceHtmlInput == undefined) {
103 alert("Source input empty or not saved");
104 return;
105 }
106
107 // Remove any html tags that will be present in input data
108 const response = await getSummary({
109 summaryInputText: sourceHtmlInput.replace(/<\/?[^>]+(>|$)/g, ""),
110 minLength: this.minLength,
111 maxLength: this.maxLength,
112 apiType: this.summaryAPIToCall,
113 }).catch((err) => {
114 console.error(err);
115 });
116
117 try {
118 const responseJson = JSON.parse(response);
119 if (responseJson?.summary) {
120 /**
121 * updateContent API to update the target field within DAF
122 * (selected on extension ui) with summary
123 *
124 */
125 //deep clone the contentBody and modify the field/proeprty to push the value in the daf
126 const contentBodyModify = JSON.parse(JSON.stringify(this.content.data.contentBody));
127 //change the value of user changed field
128 contentBodyModify[this.dafOutputField] = responseJson?.summary;
129 updateContent({
130 contentBody: contentBodyModify,
131 }).then(() => {
132 //callback after daf update
133 });
134 } else {
135 console.log("unable to get summary", responseJson?.message);
136 }
137 } catch (e) {
138 console.error(e);
139 }
140 }
141
142 _isTextType(ref) {
143 return (
144 ref === "#/$defs/sfdc_cms__textType" ||
145 ref === "#/$defs/sfdc_cms__richTextType" ||
146 ref === "#/$defs/sfdc_cms__multilineTextType"
147 );
148 }
149}