Build Extensions for Salesforce CMS
Reference: Component Properties and JSON Structures
Sample Marketing Extension: Tone Checker
Display Salesforce CMS Content
Display CMS Content in Experience Builder Sites
For detailed, step-by-step instructions about creating an extension for marketing content types, see Build Extensions for Marketing Content in Marketing Cloud Next. These code samples are for an extension that allows marketers to use AI to edit the tone of content within a selected component on the canvas.
This sample Apex controller contains the secure callout using named credentials (GeminiNC), and it connects the extension to Gemini. It can revise content in a selected component to match a specified tone.
1public with sharing class ToneChecker {
2
3 // System prompt for generating email blocks in CNAVS structure format
4 private static final String TONE_CHECKER_SYSTEM_PROMPT =
5 'You are a tone transformation assistant. Your task is to rewrite content in a specified tone while preserving the original meaning, intent, and key details.\n\n' +
6 'You will receive the following input parameters:\n' +
7 'Text: {text}\n' +
8 'TargetTone: {target_tone}\n\n' +
9 'Guidelines:\n' +
10 '- Do not add or remove important information.\n' +
11 '- Change only the tone, vocabulary, and sentence style.\n' +
12 '- Keep the output clear, natural, and concise.\n' +
13 '- Do not include explanations or extra formatting.\n' +
14 '- Return only the rewritten text.\n\n' +
15 'Supported tones:\n' +
16 '- Energetic\n' +
17 '- Professional\n' +
18 '- Straight\n\n' +
19 'Always rewrite the provided text strictly in the requested tone and output only the transformed text.';
20
21 /**
22 * Change tone of text content using Gemini AI
23 * Returns tone corrected text
24 * @param targetTone - The target tone selected by the user
25 * @param inputText - The text input selected by the user
26 * @return String - Tone corrected text
27 */
28 @AuraEnabled
29 public static String changeTone(String targetTone, String inputText) {
30 try {
31 // Replace placeholders
32 String fullPrompt = TONE_CHECKER_SYSTEM_PROMPT
33 .replace('{text}', inputText)
34 .replace('{target_tone}', targetTone);
35
36 String result = callGeminiAPI(fullPrompt, 8192);
37 return result;
38
39 } catch (Exception e) {
40 System.debug('Error in changeTone: ' + e.getMessage());
41 throw new AuraHandledException('Error changing tone of text: ' + e.getMessage());
42 }
43 }
44
45 /**
46 * Call Gemini AI API to change tone of content
47 * @param prompt - The prompt text to send
48 * @param maxOutputTokens - Maximum number of tokens for the response
49 * @return String - The generated text from Gemini AI
50 */
51 private static String callGeminiAPI(String prompt, Integer maxOutputTokens) {
52 try {
53 // Build Gemini API request structure
54 Map<String, Object> geminiRequest = new Map<String, Object>{
55 'contents' => new List<Object>{
56 new Map<String, Object>{
57 'parts' => new List<Object>{
58 new Map<String, Object>{
59 'text' => prompt
60 }
61 }
62 }
63 },
64 'generationConfig' => new Map<String, Object>{
65 'temperature' => 0.7,
66 'maxOutputTokens' => maxOutputTokens
67 }
68 };
69
70 // Create HTTP request
71 HttpRequest req = new HttpRequest();
72 req.setEndpoint('callout:GeminiNC');
73 req.setHeader('Content-Type', 'application/json');
74 req.setMethod('POST');
75 // Increase timeout to 60 or 120 seconds
76 req.setTimeout(120000);
77
78 // Set request body
79 String requestBody = JSON.serialize(geminiRequest);
80 req.setBody(requestBody);
81
82 System.debug('Gemini API Request Body: ' + requestBody);
83
84 // Make the callout
85 Http http = new Http();
86 HttpResponse res = http.send(req);
87
88 System.debug('Response Status Code: ' + res.getStatusCode());
89 System.debug('Response Body: ' + res.getBody());
90
91 // Handle response
92 Integer statusCode = res.getStatusCode();
93
94 if (statusCode == 200) {
95 return parseGeminiResponse(res.getBody());
96 } else if (statusCode == 429) {
97 // Rate limit exceeded - parse error for details
98 String errorMessage = parseErrorResponse(res.getBody());
99 throw new CalloutException('Rate limit exceeded. ' + errorMessage + ' Please wait a moment and try again.');
100 } else if (statusCode == 400) {
101 String errorMessage = parseErrorResponse(res.getBody());
102 throw new CalloutException('Bad request: ' + errorMessage);
103 } else if (statusCode == 401 || statusCode == 403) {
104 throw new CalloutException('Authentication failed. Please check your Gemini API key configuration.');
105 } else {
106 String errorMessage = parseErrorResponse(res.getBody());
107 throw new CalloutException('API error (Status ' + statusCode + '): ' + errorMessage);
108 }
109
110 } catch (Exception e) {
111 System.debug('Error calling Gemini API: ' + e.getMessage());
112 throw new CalloutException('Gemini API Error: ' + e.getMessage());
113 }
114 }
115
116 /**
117 * Parse the Gemini API response and extract the generated text
118 * Expected response structure:
119 * {
120 * "candidates": [{
121 * "content": {
122 * "parts": [{ "text": "generated text" }]
123 * }
124 * }]
125 * }
126 * @param responseBody - The raw JSON response from Gemini API
127 * @return String - The extracted generated text
128 */
129 private static String parseGeminiResponse(String responseBody) {
130 Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
131
132 if (responseMap.containsKey('candidates')) {
133 List<Object> candidates = (List<Object>) responseMap.get('candidates');
134
135 if (candidates != null && !candidates.isEmpty()) {
136 Map<String, Object> firstCandidate = (Map<String, Object>) candidates[0];
137
138 if (firstCandidate.containsKey('content')) {
139 Map<String, Object> content = (Map<String, Object>) firstCandidate.get('content');
140
141 if (content.containsKey('parts')) {
142 List<Object> parts = (List<Object>) content.get('parts');
143
144 if (parts != null && !parts.isEmpty()) {
145 Map<String, Object> firstPart = (Map<String, Object>) parts[0];
146
147 if (firstPart.containsKey('text')) {
148 String generatedText = (String) firstPart.get('text');
149 System.debug('Successfully extracted text from Gemini response');
150 return generatedText;
151 }
152 }
153 }
154 }
155 }
156 }
157
158 throw new CalloutException('Unable to extract text from Gemini response. Unexpected structure.');
159 }
160
161 /**
162 * Parse error response from Gemini API
163 * Expected error structure:
164 * {
165 * "error": {
166 * "code": 429,
167 * "message": "Resource has been exhausted...",
168 * "status": "RESOURCE_EXHAUSTED"
169 * }
170 * }
171 * @param responseBody - The raw JSON error response
172 * @return String - The extracted error message
173 */
174 private static String parseErrorResponse(String responseBody) {
175 try {
176 Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
177
178 if (responseMap.containsKey('error')) {
179 Map<String, Object> errorObj = (Map<String, Object>) responseMap.get('error');
180
181 if (errorObj.containsKey('message')) {
182 return (String) errorObj.get('message');
183 }
184
185 if (errorObj.containsKey('status')) {
186 return (String) errorObj.get('status');
187 }
188 }
189
190 return responseBody;
191 } catch (Exception ex) {
192 return responseBody;
193 }
194 }
195}In this sample, the Tone Checker extension is made available only to heading components within email content. When opened, the extension appears in a 640x600 px floating panel within the email builder.
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 increased for the current release -->
4 <apiVersion>66.0</apiVersion>
5 <isExposed>true</isExposed>
6 <masterLabel>Tone Checker</masterLabel>
7 <description>Refine text with AI on Heading for email content</description>
8 <targets>
9 <target>lightning__CmsEditorExtension</target>
10 </targets>
11 <targetConfigs>
12 <targetConfig targets="lightning__CmsEditorExtension">
13 <size width="x-large" height="600"></size>
14 <contentTypes>
15 <contentType fullyQualifiedName="sfdc_cms__email">
16 <blockTypes>
17 <blockType fullyQualifiedName="lightning__heading"></blockType>
18 </blockTypes>
19 </contentType>
20 </contentTypes>
21 </targetConfig>
22</targetConfigs>
23</LightningComponentBundle>In this sample, the Tone Checker extension UI includes radio buttons for tone selection, a preview container for the revised content, and action buttons that the user can click to apply, to try generating content again, or to replace the content with the revised version.
1<template>
2 <div class="slds-card">
3 <div class="slds-form">
4 <h2 class="slds-text-heading_medium slds-m-bottom_medium">Change Tone</h2>
5
6 <!-- Radio button group for tone selection -->
7 <lightning-radio-group
8 name="toneOptions"
9 label=""
10 options="{toneOptions}"
11 value="{selectedTone}"
12 onchange="{handleToneChange}"
13 type="radio"
14 >
15 </lightning-radio-group>
16
17 <!-- Preview text container -->
18 <div class="preview-container slds-m-top_medium">
19 <div class="preview-text">
20 {previewText}
21 </div>
22 </div>
23
24 <!-- Action buttons -->
25 <div class="slds-m-top_medium slds-align_absolute-center">
26 <lightning-button
27 label="Try again"
28 icon-name="utility:refresh"
29 onclick="{handleTryAgain}"
30 class="slds-m-bottom_small"
31 >
32 </lightning-button>
33 </div>
34
35 <div class="slds-align_absolute-center">
36 <lightning-button variant="brand" label="Replace" onclick="{handleReplace}">
37 </lightning-button>
38 </div>
39 </div>
40 </div>
41</template>Use this sample CSS file to style your Tone Checker extension. If you don’t use this CSS file, your extension automatically inherits Salesforce Lightning Design System (SLDS) styles.
1.preview-container {
2 border: 1px solid #dddbda;
3 border-radius: 0.25rem;
4 padding: 1rem;
5 min-height: 150px;
6 background-color: #ffffff;
7 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
8}
9
10.preview-text {
11 font-size: 0.875rem;
12 line-height: 1.5;
13 color: #3e3e3c;
14 white-space: pre-wrap;
15 word-wrap: break-word;
16}
17
18.slds-card {
19 padding: 1rem;
20}
21
22.slds-text-heading_medium {
23 font-size: 1.25rem;
24 font-weight: 700;
25 color: #181818;
26}This file demonstrates JavaScript that integrates with experience/blockBuilderApi methods to edit content within components. It takes the content from a selected heading component and revises it using Gemini to match the tone selected by the user. Then it can replace the content in the heading component with the revised content.
1import { LightningElement, wire } from "lwc";
2import { getCurrentSelectedBlock, replaceBlock } from "experience/blockBuilderApi";
3import changeTone from "@salesforce/apex/ToneChecker.changeTone";
4
5/**
6 * This component allows users to change the tone of content
7 * by selecting different tone options (Energetic, Professional, Straight)
8 */
9export default class ToneChecker extends LightningElement {
10 selectedTone = "";
11 previewText = "";
12 originalText = "";
13 block;
14 isLoading = false;
15
16 @wire(getCurrentSelectedBlock, {})
17 onCurrentSelectedBlock({ data }) {
18 if (data) {
19 this.originalText = data.attributes.text;
20 this.block = JSON.parse(JSON.stringify(data));
21 }
22 }
23
24 get toneOptions() {
25 return [
26 { label: "Energetic", value: "energetic" },
27 { label: "Professional", value: "professional" },
28 { label: "Straight", value: "straight" },
29 ];
30 }
31
32 handleToneChange(event) {
33 this.selectedTone = event.detail.value;
34 this.generateTonedText();
35 }
36
37 handleTryAgain() {
38 // Regenerate the text with the same tone
39 this.generateTonedText();
40 }
41
42 handleReplace() {
43 // Replace the block text on canvas with the new toned text
44 if (this.previewText && this.block) {
45 let _id = this.block.id;
46 this.block.attributes.text = this.previewText.replace(/\n/g, "<br/>");
47
48 replaceBlock(this.block, {
49 nodeId: _id,
50 })
51 .then((response) => {
52 console.log("Block replaced successfully", response);
53 })
54 .catch((error) => {
55 console.error("Error replacing block:", error);
56 });
57 }
58 }
59
60 async generateTonedText() {
61 if (!this.selectedTone) {
62 return;
63 }
64
65 if (!this.originalText) {
66 console.error("No text available to change tone");
67 return;
68 }
69
70 this.isLoading = true;
71
72 try {
73 // Call Apex method to change tone
74 const tonedText = await changeTone({
75 targetTone: this.selectedTone,
76 inputText: this.originalText,
77 });
78
79 this.previewText = tonedText;
80 console.log("Tone changed successfully");
81 } catch (error) {
82 console.error("Error changing tone:", error);
83 this.previewText = "Error generating toned text. Please try again.";
84 } finally {
85 this.isLoading = false;
86 }
87 }
88}