-
-
Notifications
You must be signed in to change notification settings - Fork 80
Added VML rounded image corner on CTA Card #1526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThis change enhances the rendering logic for images and video thumbnails in email outputs, particularly when the Possibly related PRs
Suggested reviewers
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
packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.jsOops! 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'
packages/kg-default-nodes/lib/nodes/embed/embed-renderer.jsOops! 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'
packages/kg-default-nodes/lib/nodes/image/image-renderer.jsOops! 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'
Note ⚡️ AI Code Reviews for VS Code, Cursor, WindsurfCodeRabbit 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. Note ⚡️ Faster reviews with cachingCodeRabbit 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 ✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
Codecov ReportAttention: Patch coverage is
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. 🚀 New features to boost your workflow:
|
There was a problem hiding this 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.srcis interpolated straight into the VML string (src="${node.src}").
Ifsrccontains",>or other special chars the attribute/value boundary can break, producing invalid HTML in emails. Escaping withencodeURIor, better, building the element tree withoutinnerHTMLwould 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 defaultsTests assert
arcsize="10%"(minimal) andarcsize="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 thumbnailThe fallback
<img>inside the “!mso” branch lacks analtattribute (currentlyalt="").
Consider supplying the video title or a generic description likealt="Video thumbnail"to improve screen-reader accessibility.packages/kg-default-nodes/lib/nodes/call-to-action/calltoaction-renderer.js (1)
107-146: Consider extractingrenderImageinto a shared util
renderImageis 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
emailCTATemplatereadable
• 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
📒 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 thoroughlyAssertions cover presence/absence of VML, fallback HTML and legacy
<v:group>—great job ensuring backwards compatibility.
| 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}; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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); | ||
| } | ||
| })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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.
| <!--[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]--> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <!--[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.
| 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}`; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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> | ||
| `; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
ref https://linear.app/ghost/issue/PROD-1842/vml-tags-for-outlook-support