Build Extensions for Salesforce CMS

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.

Set up Named Credentials 

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 the Apex Controller 

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.

Example Apex Controller 

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}

Create the Lightning Web Component (LWC) 

Create the Configuration File 

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.

Height and Width Attributes 

AttributeTypeDescriptionDefault
widthenumEnter 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
heightnumberEnter 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.

Example Configuration File 

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>

Target a Specific Content Type 

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 TypeFully Qualified Name (FQN)
Audiosfdc_cms__audio
Documentsfdc_cms__document
Imagesfdc_cms__image
Newssfdc_cms__news
Videosfdc_cms__video

Example Configuration File for News Content 

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>

Create the HTML Template 

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.

Example HTML File 

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>

Create the JavaScript File 

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.

Example JavaScript File 

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}

See Also