Skip to content

Conversation

@ronaldlangeveld
Copy link
Contributor

@ronaldlangeveld ronaldlangeveld commented May 22, 2025

@coderabbitai
Copy link

coderabbitai bot commented May 22, 2025

Walkthrough

This change enhances the rendering logic for images and video thumbnails in email outputs, particularly when the emailCustomizationAlpha feature flag is enabled and the imageCorners design option is set to 'rounded'. It introduces VML-based markup for Outlook compatibility, ensuring rounded corners for images in call-to-action nodes, embed nodes, and image nodes. The update centralizes image rendering logic, refines image resizing and URL rewriting, and adjusts test suites to verify the new conditional VML rendering. No exported function signatures are changed; modifications are internal to rendering functions and new tests are added.

Possibly related PRs

  • Added cropped image as minimal image in CTA Card #1453: Modifies the emailCTATemplate function in calltoaction-renderer.js to improve image handling for email CTAs, including image dimension calculation and URL rewriting for minimal layout images.
  • Added initial HTML and Email rendering #1434: Introduces and extends rendering logic for call-to-action nodes, including enhancements to emailCTATemplate and related image handling, within the same file and test suites.
  • Refactored CTA card button rendering #1517: Refactors the internal implementation of emailCTATemplate in calltoaction-renderer.js and involves the emailCustomizationAlpha feature flag, relating to image and button rendering logic in the same component.

Suggested reviewers

  • kevinansfield

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: Failed to load parser '@babel/eslint-parser' declared in 'packages/kg-default-nodes/.eslintrc.js': Cannot find module '@babel/eslint-parser'
Require stack:

  • /packages/kg-default-nodes/.eslintrc.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1405:15)
    at require.resolve (node:internal/modules/helpers:145:19)
    at Object.resolve (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2346:46)
    at ConfigArrayFactory._loadParser (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3325:39)
    at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3099:43)
    at _normalizeObjectConfigDataBody.next ()
    at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3040:20)
    at _normalizeObjectConfigData.next ()
    at ConfigArrayFactory.loadInDirectory (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2886:28)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3871:46)
packages/kg-default-nodes/lib/nodes/embed/embed-renderer.js

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: Failed to load parser '@babel/eslint-parser' declared in 'packages/kg-default-nodes/.eslintrc.js': Cannot find module '@babel/eslint-parser'
Require stack:

  • /packages/kg-default-nodes/.eslintrc.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1405:15)
    at require.resolve (node:internal/modules/helpers:145:19)
    at Object.resolve (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2346:46)
    at ConfigArrayFactory._loadParser (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3325:39)
    at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3099:43)
    at _normalizeObjectConfigDataBody.next ()
    at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3040:20)
    at _normalizeObjectConfigData.next ()
    at ConfigArrayFactory.loadInDirectory (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2886:28)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3871:46)
packages/kg-default-nodes/lib/nodes/image/image-renderer.js

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: Failed to load parser '@babel/eslint-parser' declared in 'packages/kg-default-nodes/.eslintrc.js': Cannot find module '@babel/eslint-parser'
Require stack:

  • /packages/kg-default-nodes/.eslintrc.js
    at Module._resolveFilename (node:internal/modules/cjs/loader:1405:15)
    at require.resolve (node:internal/modules/helpers:145:19)
    at Object.resolve (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2346:46)
    at ConfigArrayFactory._loadParser (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3325:39)
    at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3099:43)
    at _normalizeObjectConfigDataBody.next ()
    at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3040:20)
    at _normalizeObjectConfigData.next ()
    at ConfigArrayFactory.loadInDirectory (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2886:28)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3871:46)

Note

⚡️ AI Code Reviews for VS Code, Cursor, Windsurf

CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
Learn more here.


Note

⚡️ Faster reviews with caching

CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure Review - Disable Cache at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the Data Retention setting under your Organization Settings.
Enjoy the performance boost—your workflow just got faster.

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@codecov-commenter
Copy link

Codecov Report

Attention: Patch coverage is 94.83871% with 8 lines in your changes missing coverage. Please review.

Project coverage is 90.62%. Comparing base (36ba9ef) to head (7ae1ade).
Report is 10 commits behind head on main.

Files with missing lines Patch % Lines
.../lib/nodes/call-to-action/calltoaction-renderer.js 86.88% 4 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1526      +/-   ##
==========================================
+ Coverage   90.36%   90.62%   +0.26%     
==========================================
  Files         190      191       +1     
  Lines       17936    18178     +242     
  Branches     2027     2057      +30     
==========================================
+ Hits        16207    16473     +266     
+ Misses       1719     1690      -29     
- Partials       10       15       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ronaldlangeveld ronaldlangeveld marked this pull request as ready for review May 22, 2025 07:55
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
packages/kg-default-nodes/lib/nodes/image/image-renderer.js (1)

100-108: Unsafe string interpolation may allow malformed markup

node.src is interpolated straight into the VML string (src="${node.src}").
If src contains ", > or other special chars the attribute/value boundary can break, producing invalid HTML in emails. Escaping with encodeURI or, better, building the element tree without innerHTML would mitigate this.

- <v:fill type="frame" src="${node.src}" />
+ <v:fill type="frame" src="${encodeURI(node.src)}" />

Or construct the elements via DOM API to avoid raw HTML concatenation.

packages/kg-default-nodes/test/nodes/call-to-action.test.js (1)

513-577: Minor: hard-coded arcsize values may drift away from renderer defaults

Tests assert arcsize="10%" (minimal) and arcsize="2%" (immersive).
If design changes adjust the corner radius the tests will fail for cosmetic reasons.
Consider deriving the expected value from a shared constant or helper so tests track implementation automatically.

packages/kg-default-nodes/lib/nodes/embed/embed-renderer.js (1)

44-57: Accessibility: missing alternative text for the thumbnail

The fallback <img> inside the “!mso” branch lacks an alt attribute (currently alt="").
Consider supplying the video title or a generic description like alt="Video thumbnail" to improve screen-reader accessibility.

packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js (1)

107-146: Consider extracting renderImage into a shared util

renderImage is an inner closure that contains ~40 lines of string templates and conditional logic.
Moving it to a top-level helper (e.g. utils/render-image.js) would:

• Keep emailCTATemplate readable
• Allow reuse by future templates that need the same VML/rounded-corner logic
• Simplify unit testing (pure function with input/output)

No functional change – purely organisational.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2aade0 and 5666ab2.

📒 Files selected for processing (6)
  • packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js (4 hunks)
  • packages/kg-default-nodes/lib/nodes/embed/embed-renderer.js (1 hunks)
  • packages/kg-default-nodes/lib/nodes/image/image-renderer.js (1 hunks)
  • packages/kg-default-nodes/test/nodes/call-to-action.test.js (1 hunks)
  • packages/kg-default-nodes/test/nodes/embed.test.js (1 hunks)
  • packages/kg-default-nodes/test/nodes/image.test.js (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
packages/kg-default-nodes/lib/nodes/embed/embed-renderer.js (2)
packages/kg-default-nodes/test/nodes/embed.test.js (3)
  • options (259-261)
  • options (332-334)
  • options (345-349)
packages/kg-default-nodes/lib/nodes/video/video-renderer.js (1)
  • emailTemplateMaxWidth (95-95)
🔇 Additional comments (1)
packages/kg-default-nodes/test/nodes/embed.test.js (1)

344-367: Looks good – covers the new VML path thoroughly

Assertions cover presence/absence of VML, fallback HTML and legacy <v:group>—great job ensuring backwards compatibility.

Comment on lines +99 to +131
if (options?.feature?.emailCustomizationAlpha &&
options?.design?.imageCorners === 'rounded') {
// Create VML wrapper element
const vmlWrapper = document.createElement('div');
vmlWrapper.innerHTML = `<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="2%" style="v-text-anchor:middle; width:${imageDimensions.width}px; height:${imageDimensions.height}px; margin-top:20px;" stroked="f">
<v:fill type="frame" src="${node.src}" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->`;

// Add the VML wrapper first
figure.appendChild(vmlWrapper);

// Then add the image with email dimensions
img.setAttribute('width', imageDimensions.width);
img.setAttribute('height', imageDimensions.height);
figure.appendChild(img);

// Add closing conditional comment
const closingComment = document.createElement('div');
closingComment.innerHTML = '<!--<![endif]-->';
figure.appendChild(closingComment);

// Skip the normal image append since we've already done it
if (!node.href) {
if (node.caption) {
const caption = document.createElement('figcaption');
caption.innerHTML = node.caption;
figure.appendChild(caption);
}
return {element: figure};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Duplicate <img> rendered when node.href is present in VML path

Inside the VML branch you append img to figure (line 113) and only skip the default append when !node.href.
If node.href is set, execution continues to the anchor-handling block (lines 152-159) and appends the same img element a second time, once inside the <v:roundrect> and again inside <a>, resulting in two images in the final markup.

Apart from needless bloat, this breaks layout in several email clients and fails the intention of wrapping the image in the link and the VML container.

@@
-            // Skip the normal image append since we've already done it
-            if (!node.href) {
-                if (node.caption) {
-                    const caption = document.createElement('figcaption');
-                    caption.innerHTML = node.caption;
-                    figure.appendChild(caption);
-                }
-                return {element: figure};
-            }
+            // When VML is used we've already inserted the image.
+            // If a link is required, wrap the existing image instead of duplicating it.
+            if (node.href) {
+                const a = document.createElement('a');
+                a.setAttribute('href', node.href);
+                a.appendChild(img);           // move, don’t duplicate
+                figure.replaceChild(a, img);  // replace existing img with anchor-wrapped img
+            }
+
+            if (node.caption) {
+                const caption = document.createElement('figcaption');
+                caption.innerHTML = node.caption;
+                figure.appendChild(caption);
+            }
+            return {element: figure};

(One of several possible fixes—pick whichever best fits your style.)
Please add a test with href set to ensure only one <img> is present.

Also applies to: 152-159

🤖 Prompt for AI Agents
In packages/kg-default-nodes/lib/nodes/image/image-renderer.js between lines 99
and 131, the img element is appended to figure inside the VML conditional block
regardless of node.href, and then appended again later if node.href is present,
causing duplicate images. To fix this, modify the logic so that when node.href
is set, the img element is only appended once inside the anchor tag within the
VML wrapper, avoiding duplication. Also, add a test case with href set to verify
that only one img element is rendered in the final markup.

Comment on lines +319 to +360
// Check for proper structure and order of elements
const expectedElements = [
'<!--[if gte mso 9]>',
'<v:roundrect',
'arcsize="2%"',
'width:600px',
'height:400px',
'margin-top:20px',
`src="${dataset.src}"`,
'</v:roundrect>',
'<![endif]-->',
'<!--[if !mso]><!-->'
];

// Verify all expected elements are present and in order
let lastIndex = -1;
expectedElements.forEach((expectedElement) => {
const currentIndex = output.indexOf(expectedElement);
currentIndex.should.be.above(-1, `Expected to find ${expectedElement}`);
currentIndex.should.be.above(lastIndex, `Expected ${expectedElement} to come after previous element`);
lastIndex = currentIndex;
});

// Verify the image element is present with correct attributes
output.should.containEql('<img');
output.should.containEql('class="kg-image"');
output.should.match(/width="600"/);
output.should.match(/height="400"/);

// Verify closing conditional comment is after the image
const imgIndex = output.indexOf('</img>');
const closingCommentIndex = output.indexOf('<!--<![endif]-->');
closingCommentIndex.should.be.above(imgIndex);

// Verify caption is present and after the VML/image
if (dataset.caption) {
output.should.containEql('<figcaption>');
output.should.containEql(dataset.caption);
const captionIndex = output.indexOf('<figcaption>');
captionIndex.should.be.above(closingCommentIndex);
}
}));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Test may give false-positives because it looks for a non-existent </img> tag

HTML <img> elements are void and never emit a closing tag.
const imgIndex = output.indexOf('</img>'); therefore returns -1, and the subsequent assertion closingCommentIndex.should.be.above(imgIndex) will always pass, even when the closing conditional comment appears before the image.

Replace the sentinel with the real <img position (or use a DOM parser) so that ordering is actually verified.

-const imgIndex = output.indexOf('</img>');
+const imgIndex = output.indexOf('<img');

Consider parsing with createDocument() to check node order more robustly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check for proper structure and order of elements
const expectedElements = [
'<!--[if gte mso 9]>',
'<v:roundrect',
'arcsize="2%"',
'width:600px',
'height:400px',
'margin-top:20px',
`src="${dataset.src}"`,
'</v:roundrect>',
'<![endif]-->',
'<!--[if !mso]><!-->'
];
// Verify all expected elements are present and in order
let lastIndex = -1;
expectedElements.forEach((expectedElement) => {
const currentIndex = output.indexOf(expectedElement);
currentIndex.should.be.above(-1, `Expected to find ${expectedElement}`);
currentIndex.should.be.above(lastIndex, `Expected ${expectedElement} to come after previous element`);
lastIndex = currentIndex;
});
// Verify the image element is present with correct attributes
output.should.containEql('<img');
output.should.containEql('class="kg-image"');
output.should.match(/width="600"/);
output.should.match(/height="400"/);
// Verify closing conditional comment is after the image
const imgIndex = output.indexOf('</img>');
const closingCommentIndex = output.indexOf('<!--<![endif]-->');
closingCommentIndex.should.be.above(imgIndex);
// Verify caption is present and after the VML/image
if (dataset.caption) {
output.should.containEql('<figcaption>');
output.should.containEql(dataset.caption);
const captionIndex = output.indexOf('<figcaption>');
captionIndex.should.be.above(closingCommentIndex);
}
}));
// Verify closing conditional comment is after the image
- const imgIndex = output.indexOf('</img>');
+ const imgIndex = output.indexOf('<img');
const closingCommentIndex = output.indexOf('<!--<![endif]-->');
🤖 Prompt for AI Agents
In packages/kg-default-nodes/test/nodes/image.test.js around lines 319 to 360,
the test incorrectly searches for a non-existent closing </img> tag, causing the
ordering assertion to always pass. Replace the search for '</img>' with the
position of the actual '<img' tag in the output string to correctly verify the
order of elements. Alternatively, consider using a DOM parser like
createDocument() to parse the output and check the node order more robustly.

Comment on lines +38 to +42
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="2%" style="v-text-anchor:middle; width:${emailTemplateMaxWidth}px; height:${spacerHeight}px; margin-top:20px;" stroked="f">
<v:fill type="frame" src="${metadata.thumbnail_url}" />
</v:roundrect>
<![endif]-->
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Outlook thumbnail becomes non-clickable – add href to the VML shape

Inside the new VML branch the <v:roundrect> is rendered without an href.
In Outlook (where the VML markup is rendered) this removes the ability to click the video thumbnail and thus breaks the expected behaviour that the image opens the video URL.

-<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="2%" style="v-text-anchor:middle; width:${emailTemplateMaxWidth}px; height:${spacerHeight}px; margin-top:20px;" stroked="f">
+<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
+             href="${url}"
+             fillcolor="none"
+             arcsize="2%"
+             style="v-text-anchor:middle; width:${emailTemplateMaxWidth}px; height:${spacerHeight}px; margin-top:20px; cursor:pointer;"
+             stroked="f">

Adding href (and optionally cursor:pointer) restores click-through behaviour while keeping rounded corners.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="2%" style="v-text-anchor:middle; width:${emailTemplateMaxWidth}px; height:${spacerHeight}px; margin-top:20px;" stroked="f">
<v:fill type="frame" src="${metadata.thumbnail_url}" />
</v:roundrect>
<![endif]-->
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
href="${url}"
fillcolor="none"
arcsize="2%"
style="v-text-anchor:middle; width:${emailTemplateMaxWidth}px; height:${spacerHeight}px; margin-top:20px; cursor:pointer;"
stroked="f">
<v:fill type="frame" src="${metadata.thumbnail_url}" />
</v:roundrect>
<![endif]-->
🤖 Prompt for AI Agents
In packages/kg-default-nodes/lib/nodes/embed/embed-renderer.js around lines 38
to 42, the VML <v:roundrect> element lacks an href attribute, causing the video
thumbnail to be non-clickable in Outlook. To fix this, add an href attribute to
the <v:roundrect> tag with the video URL so the thumbnail becomes clickable, and
optionally add a style for cursor:pointer to indicate interactivity.

Comment on lines +76 to +82
if (dataset.layout === 'minimal' && dataset.imageUrl) {
if (isLocalContentImage(dataset.imageUrl, options.siteUrl) && options.canTransformImage?.(dataset.imageUrl)) {
const [, imagesPath, filename] = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/);
const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] || {width: 256, height: 256}; // default to 256 since we know the image is a square
dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Un-guarded RegExp destructuring may throw

match() can return null; destructuring [ , imagesPath, filename ] without a null-check will raise TypeError: Cannot destructure property… of null.
Even though isLocalContentImage() should filter most external URLs, it’s safer to guard against unexpected input:

-const [, imagesPath, filename] = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/);
+const match = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/);
+if (match) {
+    const [, imagesPath, filename] = match;
+    const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] ?? {width: 256, height: 256};
+    dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`;
+}

This prevents a hard crash if an unexpected path slips through.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js
around lines 76 to 82, the destructuring assignment from
dataset.imageUrl.match() is done without checking if match() returned null,
which can cause a TypeError. To fix this, store the result of match() in a
variable, check if it is not null before destructuring, and only proceed with
updating dataset.imageUrl if the match succeeded.

Comment on lines +117 to 143
if (options?.feature?.emailCustomizationAlpha && options?.design?.imageCorners === 'rounded') {
if (dataset.layout === 'minimal') {
return `
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="10%" style="v-text-anchor:middle; width:64px; height:64px;" stroked="f">
<v:fill type="frame" src="${dataset.imageUrl}" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
${wrapWithLink(dataset, `<img src="${dataset.imageUrl}" alt="CTA Image" class="kg-cta-image" width="64" height="64">`)}
<!--<![endif]-->
`;
}

if (dataset.layout === 'minimal' && dataset.imageUrl) {
if (isLocalContentImage(dataset.imageUrl, options.siteUrl) && options.canTransformImage?.(dataset.imageUrl)) {
const [, imagesPath, filename] = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/);
const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] || {width: 256, height: 256}; // default to 256 since we know the image is a square
dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`;
return `
<div style="${dataset.layout !== 'minimal' ? 'text-align: center;' : ''}">
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="2%" style="v-text-anchor:middle; width:${imageSize}px; height:${imageHeight}px; margin: 0 auto;" stroked="f">
<v:fill type="frame" src="${dataset.imageUrl}" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
${wrapWithLink(dataset, `<img src="${dataset.imageUrl}" alt="CTA Image" class="kg-cta-image" width="${imageSize}" height="${imageHeight}" ${dataset.layout !== 'minimal' ? 'style="margin: 0 auto; display: block;"' : ''}>`)}
<!--<![endif]-->
</div>
`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

VML image in Outlook is not clickable – propagate the link

Similar to the embed case, the minimal and regular layouts wrap the raster <img> in an anchor via wrapWithLink, but the VML <v:roundrect> produced for Outlook has no href.
Outlook users will therefore lose the ability to follow the CTA when they click the image.

Add href="${dataset.buttonUrl}" (only if a button URL is present) so both HTML and VML branches remain interactive:

-<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="10%" style="v-text-anchor:middle; width:64px; height:64px;" stroked="f">
+<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
+             fillcolor="none"
+             arcsize="10%"
+             ${dataset.buttonUrl ? `href="${dataset.buttonUrl}"` : ''}
+             style="v-text-anchor:middle; width:64px; height:64px; cursor:pointer;"
+             stroked="f">

Apply the same fix in the non-minimal block (arcsize 2 %).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (options?.feature?.emailCustomizationAlpha && options?.design?.imageCorners === 'rounded') {
if (dataset.layout === 'minimal') {
return `
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="10%" style="v-text-anchor:middle; width:64px; height:64px;" stroked="f">
<v:fill type="frame" src="${dataset.imageUrl}" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
${wrapWithLink(dataset, `<img src="${dataset.imageUrl}" alt="CTA Image" class="kg-cta-image" width="64" height="64">`)}
<!--<![endif]-->
`;
}
if (dataset.layout === 'minimal' && dataset.imageUrl) {
if (isLocalContentImage(dataset.imageUrl, options.siteUrl) && options.canTransformImage?.(dataset.imageUrl)) {
const [, imagesPath, filename] = dataset.imageUrl.match(/(.*\/content\/images)\/(.*)/);
const iconSize = options?.imageOptimization?.internalImageSizes?.['email-cta-minimal-image'] || {width: 256, height: 256}; // default to 256 since we know the image is a square
dataset.imageUrl = `${imagesPath}/size/w${iconSize.width}h${iconSize.height}/${filename}`;
return `
<div style="${dataset.layout !== 'minimal' ? 'text-align: center;' : ''}">
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" fillcolor="none" arcsize="2%" style="v-text-anchor:middle; width:${imageSize}px; height:${imageHeight}px; margin: 0 auto;" stroked="f">
<v:fill type="frame" src="${dataset.imageUrl}" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
${wrapWithLink(dataset, `<img src="${dataset.imageUrl}" alt="CTA Image" class="kg-cta-image" width="${imageSize}" height="${imageHeight}" ${dataset.layout !== 'minimal' ? 'style="margin: 0 auto; display: block;"' : ''}>`)}
<!--<![endif]-->
</div>
`;
}
if (options?.feature?.emailCustomizationAlpha && options?.design?.imageCorners === 'rounded') {
if (dataset.layout === 'minimal') {
return `
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
fillcolor="none"
arcsize="10%"
${dataset.buttonUrl ? `href="${dataset.buttonUrl}"` : ''}
style="v-text-anchor:middle; width:64px; height:64px; cursor:pointer;"
stroked="f">
<v:fill type="frame" src="${dataset.imageUrl}" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
${wrapWithLink(dataset, `<img src="${dataset.imageUrl}" alt="CTA Image" class="kg-cta-image" width="64" height="64">`)}
<!--<![endif]-->
`;
}
return `
<div style="${dataset.layout !== 'minimal' ? 'text-align: center;' : ''}">
<!--[if gte mso 9]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml"
fillcolor="none"
arcsize="2%"
${dataset.buttonUrl ? `href="${dataset.buttonUrl}"` : ''}
style="v-text-anchor:middle; width:${imageSize}px; height:${imageHeight}px; margin: 0 auto; cursor:pointer;"
stroked="f">
<v:fill type="frame" src="${dataset.imageUrl}" />
</v:roundrect>
<![endif]-->
<!--[if !mso]><!-->
${wrapWithLink(dataset, `<img src="${dataset.imageUrl}" alt="CTA Image" class="kg-cta-image" width="${imageSize}" height="${imageHeight}" ${dataset.layout !== 'minimal' ? 'style="margin: 0 auto; display: block;"' : ''}>`)}
<!--<![endif]-->
</div>
`;
}
🤖 Prompt for AI Agents
In packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js
between lines 117 and 143, the VML <v:roundrect> elements used for Outlook
rendering lack an href attribute, making the images non-clickable in Outlook. To
fix this, add href="${dataset.buttonUrl}" to the <v:roundrect> tags in both the
minimal and non-minimal layout blocks, but only if dataset.buttonUrl is present.
This ensures that the VML shapes are clickable and consistent with the HTML
anchor wrapping the <img> elements.

@ErisDS ErisDS closed this Jun 18, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants