Build Extensions for Marketing Content in Marketing Cloud Next

In Marketing Cloud Next, build extensions that are compatible with marketing content types and components to boost productivity. Marketers can use extensions to draft, revise, or customize the entire content body or individual components within a marketing asset.

You can build extensions that work with any third-party tool. This topic explains how to build an extension that integrates with an external AI service to generate or refine marketing copy. 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. In this topic, the extension is connected to Gemini.

Considerations 

In the content builder, extensions work best with non-personalized content—content that doesn’t include merge fields or dynamic content variations. We recommend that content authors add merge fields or create dynamic variations after they use an extension.

Most marketing content types support extensions except for expressions and form handlers. Forms support extensions only for components within the content body, and not for fields.

In this documentation, “components” and “blocks” refer to the same concept. What are called “components” in the marketing content builder UI are “blocks” in the code.

Note

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 GeminiNC. 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 Gemini 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.

In the Apex controller, you can transform the generative AI output to make sure that it’s compatible with structured marketing content. For example, to build a fully styled Paragraph component with your extension, use the Apex controller to map the AI response directly into the JSON schema of a paragraph component. This allows the extension to return a complete, editable component rather than text only. For information about component JSON schemas, see Reference: Component Properties and JSON Structures.

Example Apex Controller 

This sample Apex controller contains the secure callout, and it connects the extension to a generative AI service, in this case, Gemini. It can return a fully structured marketing email including a subject line, preheader, HTML, and structured components or blocks.

1public with sharing class EmailAIAssistant {
2
3    // System prompt for generating email blocks in CNAVS structure format
4    private static final String EMAIL_BLOCKS_SYSTEM_PROMPT =
5        'You are an expert email content generator. Generate structured email blocks based on the user\'s request. ' +
6        'You MUST respond with a valid JSON object containing a subjectLine and an array of blocks.\n\n' +
7        'Response format:\n' +
8        '{\n' +
9        '  "subjectLine": "A compelling email subject line (max 60 characters)",\n' +
10        '  "blocks": [ array of block objects ]\n' +
11        '}\n\n' +
12        'Available block types and their exact structure:\n\n' +
13        ' add text color ,textDecorationLine, fontsize; and padding as required so that the email blocks look good and professional\n'+
14        '1. HEADING block (for h1, h2, h3, etc.):\n' +
15        '{\n' +
16        '  "attributes": {\n' +
17        '    "align": "left",\n' +
18        '    "lightning:colorGroup": {\n' +
19        '      "textColor": "add hex code for color to look good and professional"\n' +
20        '    },\n' +
21        '   "lightning:padding": {\n'+
22        '       "bottom": {\n' +
23        '           "unit": "px",\n' +
24        '           "value": 16.0 \n' +
25        '       },\n' +
26        '       "left": {\n' +
27        '           "unit": "px",\n'+
28        '           "value": 16.0\n' +
29        '       },\n' +
30        '       "right": {\n' +
31        '           "unit": "px",\n' +
32        '           "value": 16.0\n' +
33        '       },\n' +
34        '       "top": {\n'+
35        '           "unit": "px",\n' +
36        '           "value": 16.0\n' +
37        '       }\n' +
38        '   }\n' +
39
40        '   "lightning:typography": {\n' +
41        '        "fontFamily": "{!$brand.fontFamily.arial}",\n' +
42        '        "fontSize": {\n' +
43        '            "unit": "px",\n' +
44        '            "value": 18.0\n' +
45        '        },\n' +
46        '        "fontWeight": "{!$brand.fontWeight.bold}",\n' +
47        '        "letterSpacing": "normal",\n' +
48        '        "lineHeight": 1.5,\n' +
49        '        "textDecoration": {\n' +
50        '            "textDecorationLine": [\n' +
51        '                "underline"\n' +
52        '            ]\n' +
53        '        },\n' +
54        '        "textTransform": "none"\n' +
55        '   }\n'+
56        '    "text": "YOUR HEADING TEXT HERE"\n' +
57        '  },\n' +
58        '  "definition": "lightning/heading",\n' +
59        '  "type": "block"\n' +
60        '}\n\n' +
61        '2. PARAGRAPH block:\n' +
62        '{\n' +
63        '  "attributes": {\n' +
64        '    "align": "left",\n' +
65        '    "lightning:colorGroup": {\n' +
66        '      "textColor": "add hex code for color to look good and professional"\n' +
67        '    },\n' +
68        '   "lightning:padding": {\n'+
69        '       "bottom": {\n' +
70        '           "unit": "px",\n' +
71        '           "value": 16.0 \n' +
72        '       },\n' +
73        '       "left": {\n' +
74        '           "unit": "px",\n'+
75        '           "value": 16.0\n' +
76        '       },\n' +
77        '       "right": {\n' +
78        '           "unit": "px",\n' +
79        '           "value": 16.0\n' +
80        '       },\n' +
81        '       "top": {\n'+
82        '           "unit": "px",\n' +
83        '           "value": 16.0\n' +
84        '       }\n' +
85        '   },\n' +
86
87        '   "lightning:typography": {\n' +
88        '        "fontFamily": "{!$brand.fontFamily.arial}",\n' +
89        '        "fontSize": {\n' +
90        '            "unit": "px",\n' +
91        '            "value": 18.0\n' +
92        '        },\n' +
93        '        "fontWeight": "{!$brand.fontWeight.bold}",\n' +
94        '        "letterSpacing": "normal",\n' +
95        '        "lineHeight": 1.5,\n' +
96        '        "textDecoration": {\n' +
97        '            "textDecorationLine": [\n' +
98        '                "underline"\n' +
99        '            ]\n' +
100        '        },\n' +
101        '        "textTransform": "none"\n' +
102        '   },\n'+
103        '    "text": "YOUR PARAGRAPH TEXT HERE"\n' +
104        '  },\n' +
105        '  "definition": "lightning/paragraph",\n' +
106        '  "type": "block"\n' +
107        '}\n\n' +
108'3. HTML block (lightning__html) for custom images, links, and stylized text:\n' +
109    '{\n' +
110    '  "type": "block",\n' +
111    '  "definition": "lightning/html",\n' +
112    '  "attributes": {\n' +
113    '    "rawHtml": "The text body of the HTML component containing HTML code",\n' +
114    '    "lightning:colorGroup": {\n' +
115    '      "backgroundColor": "{!$brand.colorScheme.root}",\n' +
116    '      "textColor": "{!$brand.colorScheme.contrast}",\n' +
117    '      "linkColor": "{!$brand.colorScheme.primaryAccent}",\n' +
118    '      "borderColor": "{!$brand.colorScheme.neutral}"\n' +
119    '    },\n' +
120    '    "lightning:padding": "{!$brand.spacing.none}",\n' +
121    '    "lightning:margin": "{!$brand.spacing.none}",\n' +
122    '    "lightning:borderRadius": "{!$brand.borderRadius.square}",\n' +
123    '    "lightning:borderWidth": "{!$brand.borderWeight.none}"\n' +
124    '  }\n' +
125    '}\n\n' +
126    'Rules:\n' +
127    '1. Use "rawHtml" for the content of the HTML block. \n' +
128    '2. Ensure rawHtml contains valid, email-safe HTML code (images, links, styled spans). \n' +
129    '3. Keep text content engaging and professional.\n' +
130    '4. IMPORTANT: Return ONLY the JSON object, no markdown code blocks, no explanations.\n\n';
131
132    /**
133     * Generate email blocks (heading, paragraph, list) using Gemini AI
134     * Returns structured blocks that can be injected into CNAVS email structure
135     * @param prompt - The user's prompt describing what email content to generate
136     * @return String - JSON containing subjectLine and blocks array
137     */
138    @AuraEnabled
139    public static String generateEmailBlocks(String prompt) {
140        try {
141            if (String.isBlank(prompt)) {
142                throw new AuraHandledException('Prompt is required');
143            }
144
145            // Combine system prompt with user prompt
146            String fullPrompt = EMAIL_BLOCKS_SYSTEM_PROMPT + 'User Request:\n' + prompt;
147
148            String result = callGeminiAPI(fullPrompt, 8192);
149            // Clean up any markdown code blocks if present
150            return cleanHtmlResponse(result);
151
152        } catch (Exception e) {
153            System.debug('Error in generateEmailBlocks: ' + e.getMessage());
154            throw new AuraHandledException('Error generating email blocks: ' + e.getMessage());
155        }
156    }
157
158    /**
159     * Call Gemini AI API to generate content
160     * @param prompt - The prompt text to send
161     * @param maxOutputTokens - Maximum number of tokens for the response
162     * @return String - The generated text from Gemini AI
163     */
164    private static String callGeminiAPI(String prompt, Integer maxOutputTokens) {
165        try {
166            // Build Gemini API request structure
167            Map<String, Object> geminiRequest = new Map<String, Object>{
168                'contents' => new List<Object>{
169                    new Map<String, Object>{
170                        'parts' => new List<Object>{
171                            new Map<String, Object>{
172                                'text' => prompt
173                            }
174                        }
175                    }
176                },
177                'generationConfig' => new Map<String, Object>{
178                    'temperature' => 0.7,
179                    'maxOutputTokens' => maxOutputTokens
180                }
181            };
182
183            // Create HTTP request
184            HttpRequest req = new HttpRequest();
185            req.setEndpoint('callout:GeminiNC');
186            req.setHeader('Content-Type', 'application/json');
187            req.setMethod('POST');
188            // Increase timeout to 60 or 120 seconds
189            req.setTimeout(120000);
190
191            // Set request body
192            String requestBody = JSON.serialize(geminiRequest);
193            req.setBody(requestBody);
194
195            System.debug('Gemini API Request Body: ' + requestBody);
196
197            // Make the callout
198            Http http = new Http();
199            HttpResponse res = http.send(req);
200
201            System.debug('Response Status Code: ' + res.getStatusCode());
202            System.debug('Response Body: ' + res.getBody());
203
204            // Handle response
205            Integer statusCode = res.getStatusCode();
206
207            if (statusCode == 200) {
208                return parseGeminiResponse(res.getBody());
209            } else if (statusCode == 429) {
210                // Rate limit exceeded - parse error for details
211                String errorMessage = parseErrorResponse(res.getBody());
212                throw new CalloutException('Rate limit exceeded. ' + errorMessage + ' Please wait a moment and try again.');
213            } else if (statusCode == 400) {
214                String errorMessage = parseErrorResponse(res.getBody());
215                throw new CalloutException('Bad request: ' + errorMessage);
216            } else if (statusCode == 401 || statusCode == 403) {
217                throw new CalloutException('Authentication failed. Please check your Gemini API key configuration.');
218            } else {
219                String errorMessage = parseErrorResponse(res.getBody());
220                throw new CalloutException('API error (Status ' + statusCode + '): ' + errorMessage);
221            }
222
223        } catch (Exception e) {
224            System.debug('Error calling Gemini API: ' + e.getMessage());
225            throw new CalloutException('Gemini API Error: ' + e.getMessage());
226        }
227    }
228
229    /**
230     * Parse the Gemini API response and extract the generated text
231     * Expected response structure:
232     * {
233     *   "candidates": [{
234     *     "content": {
235     *       "parts": [{ "text": "generated text" }]
236     *     }
237     *   }]
238     * }
239     * @param responseBody - The raw JSON response from Gemini API
240     * @return String - The extracted generated text
241     */
242    private static String parseGeminiResponse(String responseBody) {
243        Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
244
245        if (responseMap.containsKey('candidates')) {
246            List<Object> candidates = (List<Object>) responseMap.get('candidates');
247
248            if (candidates != null && !candidates.isEmpty()) {
249                Map<String, Object> firstCandidate = (Map<String, Object>) candidates[0];
250
251                if (firstCandidate.containsKey('content')) {
252                    Map<String, Object> content = (Map<String, Object>) firstCandidate.get('content');
253
254                    if (content.containsKey('parts')) {
255                        List<Object> parts = (List<Object>) content.get('parts');
256
257                        if (parts != null && !parts.isEmpty()) {
258                            Map<String, Object> firstPart = (Map<String, Object>) parts[0];
259
260                            if (firstPart.containsKey('text')) {
261                                String generatedText = (String) firstPart.get('text');
262                                System.debug('Successfully extracted text from Gemini response');
263                                return generatedText;
264                            }
265                        }
266                    }
267                }
268            }
269        }
270
271        throw new CalloutException('Unable to extract text from Gemini response. Unexpected structure.');
272    }
273
274    /**
275     * Clean up the response by removing markdown code blocks if present
276     * @param response - The raw response text
277     * @return String - Cleaned content
278     */
279    private static String cleanHtmlResponse(String response) {
280        if (String.isBlank(response)) {
281            return response;
282        }
283
284        String cleaned = response.trim();
285
286        // Remove markdown code blocks (```json ... ```, ```html ... ```, or ``` ... ```)
287        if (cleaned.startsWith('```json')) {
288            cleaned = cleaned.removeStart('```json').trim();
289        } else if (cleaned.startsWith('```html')) {
290            cleaned = cleaned.removeStart('```html').trim();
291        } else if (cleaned.startsWith('```')) {
292            cleaned = cleaned.removeStart('```').trim();
293        }
294
295        // Also handle case where just "json" prefix exists (without backticks)
296        if (cleaned.startsWith('json\n')) {
297            cleaned = cleaned.removeStart('json').trim();
298        }
299
300        if (cleaned.endsWith('```')) {
301            cleaned = cleaned.removeEnd('```').trim();
302        }
303
304        return cleaned;
305    }
306
307    /**
308     * Parse error response from Gemini API
309     * Expected error structure:
310     * {
311     *   "error": {
312     *     "code": 429,
313     *     "message": "Resource has been exhausted...",
314     *     "status": "RESOURCE_EXHAUSTED"
315     *   }
316     * }
317     * @param responseBody - The raw JSON error response
318     * @return String - The extracted error message
319     */
320    private static String parseErrorResponse(String responseBody) {
321        try {
322            Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(responseBody);
323
324            if (responseMap.containsKey('error')) {
325                Map<String, Object> errorObj = (Map<String, Object>) responseMap.get('error');
326
327                if (errorObj.containsKey('message')) {
328                    return (String) errorObj.get('message');
329                }
330
331                if (errorObj.containsKey('status')) {
332                    return (String) errorObj.get('status');
333                }
334            }
335
336            return responseBody;
337        } catch (Exception ex) {
338            return responseBody;
339        }
340    }
341}

Create the Lightning Web Component (LWC) 

Create the Configuration File 

To make your extension visible in the marketing content builder, set targets and targetConfigs in the extension’s configuration or .js-meta.xml file. The primary target is lightning__CmsEditorExtension, which makes the extension appear in the extensions menu in the CMS content builder.

Set the targetConfig 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 Value
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 px and 600 px.400 px

An extension with this target configuration is visible to all CMS and marketing content types that support extensions. For a basic code sample, see Build Extensions for Salesforce CMS.

To make your extension available only to a specific marketing content type, such as an email or landing page, specify the contentTypes under targetconfig for lightning__CmsEditorExtension. This allows your extension to interact with the entire canvas and any other properties of the content type that you’re targeting, such as an email’s subject line and preheader. 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.

Marketing Content Fully Qualified Names

This table contains the FQNs of only the marketing content types that support extensions.

Content TypeFully Qualified Name (FQN)
Audiosfdc_cms__audio
Brandsfdc_cms__brand
Content Block: Emailsfdc_cms__emailFragment
Content Block: Landing Pagesfdc_cms__webFragment
Documentsfdc_cms__document
Emailsfdc_cms__email
Email Templatesfdc_cms__emailTemplate
Formsfdc_cms__form
Imagesfdc_cms__image
In-App Messagesfdc_cms__inApp
Landing Pagesfdc_cms__landingPage
Landing Page Templatesfdc_cms__landingPageTemplate
SMS Messagesfdc_cms__sms
Tracked Linksfdc_cms__trackedLink
WhatsApp Sessionsfdc_cms__whatsappSession
Videosfdc_cms__video

Example Configuration File 

This example shows the configuration file of a generative AI extension that’s available only to 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    <apiVersion>66.0</apiVersion>
4    <isExposed>true</isExposed>
5    <masterLabel>Email AI Assistant</masterLabel>
6    <description>Generate structured email blocks (h1, paragraph, ul/li) using Gemini AI that can be injected to the canvas.</description>
7    <targets>
8        <target>lightning__CmsEditorExtension</target>
9    </targets>
10    <targetConfigs>
11        <targetConfig targets="lightning__CmsEditorExtension">
12            <size width="x-large" height="600"></size>
13            <contentTypes>
14                <contentType fullyQualifiedName="sfdc_cms__email">
15                </contentType>
16            </contentTypes>
17        </targetConfig>
18    </targetConfigs>
19</LightningComponentBundle>

Targeting a Specific Component 

To make your extension available only to a specific component, specify the blockTypes under targetconfig for lightning__CmsEditorExtension. To make an extension available to all supported component types, don’t target the extension to any specific components.

To target the extension to a component type, set the blockType fullyQualifiedName to the fully qualified name (FQN) of the component you want to target. For example, to target the AI content generator to a paragraph component, set the blockType fullyQualifiedName to lightning__paragraph. To target multiple component types, list multiple components under the targetconfig or, if you’re also targeting a specific content type, list the components under the content type.

Not all components are available in all content types. If you target the extension to both a component type and a content type, make sure that the component is available in the specified content type. For example, you can’t target a paragraph component in an image content type.

Note

If you specify a component type, but not a content type, your extension is available to the component in all content types that support it. For example, if you target a heading component without specifying any content types, your extension is available to heading components in emails, landing pages, content blocks, templates, and forms.

Fully Qualified Names of Components in Marketing Content

This table contains the FQNs only of the component types that support extensions.

ComponentFully Qualified Name (FQN)
Headinglightning__heading
HTMLlightning__html
Imagelightning__image
Listlightning__list
Paragraphlightning__paragraph
Buttonlightning__button
Dividerlightning__divider
Sectionlightning__section
Columnlightning__column

Example Configuration File 

This example shows the configuration file of a generative AI extension that’s available only to paragraph components in the email and landing page content types.

1<?xml version="1.0" encoding="UTF-8"?>
2<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3    <apiVersion>66.0</apiVersion>
4    <isExposed>true</isExposed>
5    <masterLabel>Email AI Assistant</masterLabel>
6    <targets>
7        <target>lightning__CmsEditorExtension</target>
8    </targets>
9    <targetConfigs>
10        <targetConfig targets="lightning__CmsEditorExtension">
11		<size height="400" width="large"></size>
12           <contentTypes>
13		  <contentType fullyQualifiedName="sfdc_cms__email">
14                   <blockTypes>
15                        <blockType fullyQualifiedName="lightning__paragraph"></blockType>
16                   </blockTypes>
17             </contentType>
18		  <contentType fullyQualifiedName="sfdc_cms__landingPage">
19                   <blockTypes>
20                        <blockType fullyQualifiedName="lightning__paragraph"></blockType>
21                   </blockTypes>
22             </contentType>
23          </contentTypes>
24        </targetConfig>
25    </targetConfigs>
26</LightningComponentBundle>

Create the HTML File 

The HTML file uses standard LWC markup to display the extension’s user interface (UI) in the floating panel. This UI is where the marketer interacts with the extension to generate content.

Example HTML File 

In this sample, the Email AI Assistant extension UI includes an input area for an AI prompt, a button to generate content, a loading spinner, interactive controls, and two displays of the AI output.

1<template>
2  <div class="slds-card">
3    <div class="slds-form">
4      <div class="header-section">
5        <h2 class="slds-text-heading_medium">Email AI Assistant</h2>
6        <span class="gemini-badge">Powered by Google</span>
7      </div>
8
9      <p class="description">
10        Generate structured email blocks (headings, paragraphs, lists) using AI.
11      </p>
12
13      <!-- Prompt input -->
14      <div class="slds-m-top_medium">
15        <div class="section-label slds-m-bottom_x-small">Your Prompt</div>
16        <lightning-textarea
17          name="promptInput"
18          label=""
19          value="{promptText}"
20          placeholder="Describe the email content you want. E.g., 'Create a welcome email with a heading, introduction paragraph, and a list of 3 key features.'"
21          max-length="2000"
22          onchange="{handlePromptChange}"
23        >
24        </lightning-textarea>
25      </div>
26
27      <!-- Generate button -->
28      <div class="slds-m-top_medium slds-align_absolute-center">
29        <lightning-button
30          variant="brand"
31          label="Generate Email"
32          icon-name="utility:sparkles"
33          onclick="{handleGenerate}"
34          disabled="{isGenerateDisabled}"
35        >
36        </lightning-button>
37      </div>
38
39      <!-- Loading spinner -->
40      <template lwc:if="{isLoading}">
41        <div class="slds-m-top_medium slds-align_absolute-center">
42          <lightning-spinner
43            alternative-text="Generating email..."
44            size="medium"
45          ></lightning-spinner>
46        </div>
47      </template>
48
49      <!-- Generated content container -->
50      <template lwc:if="{hasGeneratedContent}">
51        <!-- Subject Line -->
52        <div class="email-field slds-m-top_medium">
53          <div class="section-label">Subject Line</div>
54          <div class="field-value">{subjectLine}</div>
55        </div>
56
57        <!-- Email Blocks -->
58        <div class="content-container slds-m-top_medium">
59          <div class="content-header">
60            <div class="section-label">Email Content</div>
61            <lightning-button
62              variant="base"
63              label="{previewButtonLabel}"
64              icon-name="{previewButtonIcon}"
65              onclick="{handleTogglePreview}"
66            >
67            </lightning-button>
68          </div>
69
70          <!-- Preview mode -->
71          <template lwc:if="{showPreview}">
72            <div class="preview-area">
73              <template for:each="{previewBlocks}" for:item="block">
74                <div key="{block.key}" class="preview-block">
75                  <template lwc:if="{block.isHeading}">
76                    <div class="preview-heading">{block.text}</div>
77                  </template>
78                  <template lwc:if="{block.isParagraph}">
79                    <div class="preview-paragraph">{block.text}</div>
80                  </template>
81                  <template lwc:if="{block.isList}">
82                    <ul class="preview-list">
83                      <template for:each="{block.items}" for:item="item">
84                        <li key="{item.key}">{item.text}</li>
85                      </template>
86                    </ul>
87                  </template>
88                </div>
89              </template>
90            </div>
91          </template>
92
93          <!-- JSON mode -->
94          <template lwc:else>
95            <div class="code-area">
96              <pre>{generatedBlocksJson}</pre>
97            </div>
98          </template>
99        </div>
100
101        <!-- Action buttons -->
102        <div class="slds-m-top_medium button-group">
103          <lightning-button
104            label="Try again"
105            icon-name="utility:refresh"
106            onclick="{handleTryAgain}"
107            class="slds-m-right_small"
108          >
109          </lightning-button>
110          <lightning-button
111            variant="brand"
112            label="Place on Canvas"
113            icon-name="utility:check"
114            onclick="{handleReplace}"
115            disabled="{isReplaceDisabled}"
116          >
117          </lightning-button>
118        </div>
119      </template>
120    </div>
121  </div>
122</template>

Create the CSS File 

The CSS file is optional. Lightning web components automatically inherit Salesforce Lightning Design System (SLDS) styles, but you can use a CSS file if you want custom styling within your extension.

Example 

Here’s an example CSS file for the Email AI Assistant extension.

1.slds-card {
2  padding: 1rem;
3  background: linear-gradient(135deg, #f8f9fa 0%, #e8f0fe 100%);
4  border-radius: 0.5rem;
5}
6
7.header-section {
8  display: flex;
9  align-items: center;
10  justify-content: space-between;
11  margin-bottom: 0.5rem;
12}
13
14.slds-text-heading_medium {
15  font-size: 1.25rem;
16  font-weight: 700;
17  color: #1a73e8;
18  margin: 0;
19}
20
21.gemini-badge {
22  font-size: 0.7rem;
23  font-weight: 500;
24  color: #5f6368;
25  background: linear-gradient(90deg, #4285f4, #ea4335, #fbbc04, #34a853);
26  background-clip: text;
27  -webkit-background-clip: text;
28  -webkit-text-fill-color: transparent;
29  padding: 0.25rem 0.5rem;
30  border: 1px solid #dadce0;
31  border-radius: 1rem;
32}
33
34.description {
35  font-size: 0.85rem;
36  color: #5f6368;
37  margin: 0.5rem 0 0 0;
38}
39
40.section-label {
41  font-size: 0.75rem;
42  font-weight: 600;
43  color: #1a73e8;
44  text-transform: uppercase;
45  letter-spacing: 0.05em;
46  margin-bottom: 0.5rem;
47}
48
49.email-field {
50  border: 1px solid #dadce0;
51  border-radius: 0.5rem;
52  padding: 0.75rem;
53  background-color: #ffffff;
54}
55
56.email-field .section-label {
57  margin-bottom: 0.25rem;
58}
59
60.field-value {
61  font-size: 0.9rem;
62  color: #3c4043;
63  line-height: 1.4;
64}
65
66.content-container {
67  border: 1px solid #1a73e8;
68  border-radius: 0.5rem;
69  padding: 1rem;
70  background-color: #ffffff;
71  box-shadow: 0 2px 8px rgba(26, 115, 232, 0.15);
72}
73
74.content-header {
75  display: flex;
76  justify-content: space-between;
77  align-items: center;
78  margin-bottom: 0.75rem;
79}
80
81.content-header .section-label {
82  margin-bottom: 0;
83}
84
85.preview-area {
86  font-size: 0.875rem;
87  line-height: 1.6;
88  color: #3c4043;
89  max-height: 300px;
90  overflow-y: auto;
91  padding: 0.75rem;
92  background-color: #fafafa;
93  border: 1px solid #e0e0e0;
94  border-radius: 0.25rem;
95}
96
97.preview-block {
98  margin-bottom: 0.75rem;
99}
100
101.preview-block:last-child {
102  margin-bottom: 0;
103}
104
105.preview-heading {
106  font-size: 1.25rem;
107  font-weight: 700;
108  color: #1a1a1a;
109  margin-bottom: 0.5rem;
110  padding-bottom: 0.5rem;
111  border-bottom: 2px solid #1a73e8;
112}
113
114.preview-paragraph {
115  font-size: 0.9rem;
116  color: #3c4043;
117  line-height: 1.6;
118}
119
120.preview-list {
121  margin: 0;
122  padding-left: 1.5rem;
123}
124
125.preview-list li {
126  font-size: 0.9rem;
127  color: #3c4043;
128  line-height: 1.8;
129  margin-bottom: 0.25rem;
130}
131
132.preview-list li:last-child {
133  margin-bottom: 0;
134}
135
136.code-area {
137  max-height: 300px;
138  overflow: auto;
139  background-color: #1e1e1e;
140  border-radius: 0.25rem;
141  padding: 0.75rem;
142}
143
144.code-area pre {
145  margin: 0;
146  font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
147  font-size: 0.75rem;
148  line-height: 1.5;
149  color: #d4d4d4;
150  white-space: pre-wrap;
151  word-wrap: break-word;
152}
153
154.button-group {
155  display: flex;
156  justify-content: center;
157  gap: 0.5rem;
158}
159
160/* Style the textarea for better UX */
161lightning-textarea {
162  --slds-c-textarea-sizing-min-height: 100px;
163}

Create the JavaScript File 

The JavaScript file defines the business logic of the extension and event handling. It can use experience/cmsEditorApi methods to read and write content in an entire marketing asset, or it can use experience/blockBuilderApi methods to read and write content within a selected component on the canvas.

The JavaScript file also manages the integration with the Apex controller to send the user’s prompt to the generative AI model and maps the resulting data into a format compatible with marketing content.

Example JavaScript File 

This is a sample JavaScript of the Email AI Assistant extension. It integrates with experience/cmsEditorApi methods and methods from the Apex controller. It takes the user input from the extension UI, sends it to the generative AI model, and programmatically updates the email to insert styled components onto the email canvas.

This sample also includes code that manages the state of the extension by using the actionstart and actiondone standard Salesforce CMS events. These events are useful to make sure that marketers don’t accidentally exit the extension before they’re finished using it.

For an example of JavaScript that integrates with experience/blockBuilderApi methods to edit content within components, see Sample Marketing Extension: Tone Checker.

1import { LightningElement, wire } from "lwc";
2import { getContent, updateContent } from "experience/cmsEditorApi";
3import generateEmailBlocks from "@salesforce/apex/EmailAIAssistant.generateEmailBlocks";
4
5/**
6 * Editor Extension component that generates email block tree using Gemini AI
7 * Generates h1, paragraph, ul/li blocks that can be injected to the canvas
8 */
9export default class EmailAIAssistant extends LightningElement {
10  contentBody = {};
11  rootBlockId;
12  sectionId;
13  columnId;
14
15  @wire(getContent, {})
16  onContent({ data }) {
17    if (data) {
18      console.log("Content Data", data);
19      this.contentBody = data.contentBody;
20      const rootBlock = data.contentBody["sfdc_cms:block"];
21      this.rootBlockId = rootBlock.id;
22
23      // Extract section and column IDs for block injection
24      if (rootBlock.children && rootBlock.children.length > 0) {
25        const section = rootBlock.children[0];
26        this.sectionId = section.id;
27        if (section.children && section.children.length > 0) {
28          this.columnId = section.children[0].id;
29        }
30      }
31    }
32  }
33
34  promptText = "";
35  generatedBlocks = null;
36  generatedBlocksJson = "";
37  subjectLine = "";
38  isLoading;
39  showPreview;
40
41  get isGenerateDisabled() {
42    return !this.promptText || this.promptText.trim() === "" || this.isLoading;
43  }
44
45  get isReplaceDisabled() {
46    return !this.generatedBlocks || this.isLoading;
47  }
48
49  get hasGeneratedContent() {
50    return this.generatedBlocks !== null;
51  }
52
53  get previewButtonLabel() {
54    return this.showPreview ? "Show JSON" : "Show Preview";
55  }
56
57  get previewButtonIcon() {
58    return this.showPreview ? "utility:code" : "utility:preview";
59  }
60
61  get previewBlocks() {
62    if (!this.generatedBlocks) return [];
63    return this.generatedBlocks.map((block, index) => this.mapBlockToPreview(block, index));
64  }
65
66  mapBlockToPreview(block, index) {
67    const baseKey = `block-${index}`;
68
69    if (block.definition === "lightning/heading") {
70      return {
71        key: baseKey,
72        isHeading: true,
73        text: block.attributes.text || "",
74        level: block.attributes.level || 1,
75      };
76    } else if (block.definition === "lightning/paragraph") {
77      return {
78        key: baseKey,
79        isParagraph: true,
80        text: block.attributes.text || "",
81      };
82    } else if (block.definition === "lightning/list") {
83      return {
84        key: baseKey,
85        isList: true,
86        items: (block.attributes.items || []).map((item, i) => ({
87          key: `${baseKey}-item-${i}`,
88          text: item,
89        })),
90      };
91    }
92    return { key: baseKey };
93  }
94
95  handlePromptChange(event) {
96    this.promptText = event.target.value;
97  }
98
99  handleGenerate() {
100    this.generateContent();
101  }
102
103  handleTryAgain() {
104    this.generateContent();
105  }
106
107  handleTogglePreview() {
108    this.showPreview = !this.showPreview;
109  }
110
111  handleReplace() {
112    let _contentBody = JSON.parse(JSON.stringify(this.contentBody));
113
114    // Get the column where we'll inject the blocks
115    const rootBlock = _contentBody["sfdc_cms:block"];
116    if (rootBlock.children && rootBlock.children.length > 0) {
117      const section = rootBlock.children[0];
118      if (section.children && section.children.length > 0) {
119        const column = section.children[0];
120        // Replace existing children with generated blocks
121        column.children = this.generatedBlocks; //this.generatedBlocksJson;
122      }
123    }
124    _contentBody["subjectLine"] = this.subjectLine;
125    updateContent({
126      contentBody: _contentBody,
127    })
128      .then(() => {
129        console.log("Email blocks injected successfully");
130        // It is safe to exit the extension now, hence dispatch the actiondone event
131        this.dispatchEvent(
132          new CustomEvent("actiondone", {
133            detail: { closeExtension: true },
134          }),
135        );
136      })
137      .catch((error) => {
138        console.error("Error updating content:", error);
139      });
140  }
141
142  async generateContent() {
143    if (!this.promptText || this.promptText.trim() === "") {
144      return;
145    }
146
147    this.isLoading = true;
148    this.generatedBlocks = null;
149    this.generatedBlocksJson = "";
150    this.subjectLine = "";
151    this.showPreview = true;
152
153    try {
154      // Call Apex method to generate email blocks
155      const generatedContent = await generateEmailBlocks({
156        prompt: this.promptText,
157      });
158
159      // Parse the JSON response
160      const parsedContent = JSON.parse(generatedContent);
161      this.subjectLine = parsedContent.subjectLine || "";
162      this.generatedBlocks = parsedContent.blocks || [];
163      this.generatedBlocksJson = JSON.stringify(this.generatedBlocks, null, 2);
164
165      console.log("Email blocks generated successfully");
166    } catch (error) {
167      console.error("Error generating email blocks:", error.message);
168      this.generatedBlocks = null;
169      this.generatedBlocksJson = "";
170      this.subjectLine = "";
171    } finally {
172      this.isLoading = false;
173      // The user has made changes that are not yet saved or finalized.
174      // Send actionstart event to prevent accidental loss of in-progress work
175      this.dispatchEvent(new CustomEvent("actionstart"));
176    }
177  }
178}

See Also