fix(email-renderer): preserve marketing images across output formats
This commit is contained in:
1 parent
371c23db46
commit
baee958c68
4 files changed
+144
-11
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
string,
|
||||
type BlockDefinition,
|
||||
type BlockNode,
|
||||
type ValueSchema,
|
||||
} from "@wrnexus/blocks";
|
||||
import { brandColor, brandFont, brandWidth } from "./brand.ts";
|
||||
|
||||
@@ -31,6 +32,24 @@ const html = (value: unknown) =>
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
const attrs = (node: BlockNode) => node.props as Record<string, unknown>;
|
||||
const imageDimension = (fallback: string): ValueSchema<string> => ({
|
||||
kind: "string",
|
||||
defaultValue: fallback,
|
||||
parse(value = fallback, path = "value") {
|
||||
return typeof value === "string" && /^\d{1,4}$/.test(value) && Number(value) > 0 && Number(value) <= 4000
|
||||
? { ok: true, value }
|
||||
: { ok: false, errors: [{code:"email.image-dimension",message:"Image dimensions must be between 1 and 4000 pixels.",severity:"error",path}] };
|
||||
},
|
||||
});
|
||||
const marketingImage = (node: BlockNode, target: "html" | "amp" | "mjml") => {
|
||||
const p = attrs(node);
|
||||
if (!p.image) return "";
|
||||
const width = html(p.width ?? "600"), height = html(p.height ?? "400");
|
||||
const src = html(p.image), alt = html(p.title);
|
||||
if (target === "amp") return `<amp-img class="marketing-image" src="${src}" alt="${alt}" width="${width}" height="${height}" layout="responsive"></amp-img>`;
|
||||
if (target === "mjml") return `<mj-image src="${src}" alt="${alt}" width="${width}px" />`;
|
||||
return `<img src="${src}" alt="${alt}" width="${width}" style="display:block;height:auto;max-width:100%;" />`;
|
||||
};
|
||||
const styleBase = (node: BlockNode, key: string) => {
|
||||
const value = (node.styles as Record<string, unknown>)[key];
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
@@ -99,7 +118,7 @@ export const emailDocumentBlock = defineBlock({
|
||||
ampEmail: ({ node, slots }) =>
|
||||
`<table role="presentation" width="100%" class="email-bg"><tr><td align="center"><table role="presentation" width="${html(attrs(node).width)}" class="email-body">${slots.content}</table></td></tr></table>`,
|
||||
mjml: ({ node, slots }) =>
|
||||
`<mjml><mj-body width="${html(attrs(node).width)}px" background-color="${html(attrs(node).background)}">${slots.content}</mj-body></mjml>`,
|
||||
`<mjml><mj-body width="${html(attrs(node).width ?? "600")}px" background-color="${html(attrs(node).background ?? "#f3f4f6")}">${slots.content}</mj-body></mjml>`,
|
||||
},
|
||||
textRenderer: ({ slots }) => slots.content,
|
||||
} satisfies BlockDefinition);
|
||||
@@ -455,11 +474,11 @@ export const feedItemBlock = leaf(
|
||||
"content.feed-item",
|
||||
"RSS article",
|
||||
"icon-[lucide--rss]",
|
||||
object({ feedUrl: safeUrl({ schemes: ["https"], optional: true }), title: string({ min: 1, max: 500 }), summary: string({ max: 2_000, default: "" }), href: safeUrl({ schemes: ["https"] }), image: safeUrl({ schemes: ["https"], optional: true }) }) as never,
|
||||
object({ width: imageDimension("600"), height: imageDimension("400"), feedUrl: safeUrl({ schemes: ["https"], optional: true }), title: string({ min: 1, max: 500 }), summary: string({ max: 2_000, default: "" }), href: safeUrl({ schemes: ["https"] }), image: safeUrl({ schemes: ["https"], optional: true }) }) as never,
|
||||
{
|
||||
emailHtml: ({ node }) => emailCell(`${attrs(node).image ? `<img src="${html(attrs(node).image)}" alt="" style="display:block;height:auto;max-width:100%;" />` : ""}<h2 style="font-family:inherit;"><a href="${html(attrs(node).href)}">${html(attrs(node).title)}</a></h2><p style="font-family:inherit;">${html(attrs(node).summary)}</p>`, node),
|
||||
ampEmail: ({ node }) => emailCell(`<h2><a href="${html(attrs(node).href)}">${html(attrs(node).title)}</a></h2><p>${html(attrs(node).summary)}</p>`, node),
|
||||
mjml: ({ node }) => `<mj-text font-size="22px" font-weight="700"><a href="${html(attrs(node).href)}">${html(attrs(node).title)}</a></mj-text><mj-text>${html(attrs(node).summary)}</mj-text>`,
|
||||
emailHtml: ({ node }) => emailCell(`${marketingImage(node, "html")}<h2 style="font-family:inherit;"><a href="${html(attrs(node).href)}">${html(attrs(node).title)}</a></h2><p style="font-family:inherit;">${html(attrs(node).summary)}</p>`, node),
|
||||
ampEmail: ({ node }) => emailCell(`${marketingImage(node, "amp")}<h2><a href="${html(attrs(node).href)}">${html(attrs(node).title)}</a></h2><p>${html(attrs(node).summary)}</p>`, node),
|
||||
mjml: ({ node }) => `${marketingImage(node, "mjml")}<mj-text font-size="22px" font-weight="700"><a href="${html(attrs(node).href)}">${html(attrs(node).title)}</a></mj-text><mj-text>${html(attrs(node).summary)}</mj-text>`,
|
||||
},
|
||||
({ node }) => `${String(attrs(node).title)}\n${String(attrs(node).summary)}\n${String(attrs(node).href)}\n\n`,
|
||||
);
|
||||
@@ -468,11 +487,11 @@ export const productBlock = leaf(
|
||||
"commerce.product",
|
||||
"Product",
|
||||
"icon-[lucide--shopping-bag]",
|
||||
object({ provider: choice(["shopify", "woocommerce", "manual"] as const, { default: "manual" }), storeUrl: safeUrl({ schemes: ["https"], optional: true }), productId: string({ max: 300, default: "" }), title: string({ min: 1, max: 500 }), price: string({ max: 100 }), image: safeUrl({ schemes: ["https"] }), href: safeUrl({ schemes: ["https"] }), buttonLabel: string({ max: 200, default: "Shop now" }) }) as never,
|
||||
object({ width: imageDimension("600"), height: imageDimension("400"), provider: choice(["shopify", "woocommerce", "manual"] as const, { default: "manual" }), storeUrl: safeUrl({ schemes: ["https"], optional: true }), productId: string({ max: 300, default: "" }), title: string({ min: 1, max: 500 }), price: string({ max: 100 }), image: safeUrl({ schemes: ["https"] }), href: safeUrl({ schemes: ["https"] }), buttonLabel: string({ max: 200, default: "Shop now" }) }) as never,
|
||||
{
|
||||
emailHtml: ({ node }) => emailCell(`<img src="${html(attrs(node).image)}" alt="${html(attrs(node).title)}" style="display:block;height:auto;max-width:100%;" /><h2 style="font-family:inherit;">${html(attrs(node).title)}</h2><p style="font-family:inherit;font-weight:700;">${html(attrs(node).price)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
||||
ampEmail: ({ node }) => emailCell(`<h2>${html(attrs(node).title)}</h2><p>${html(attrs(node).price)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
||||
mjml: ({ node }) => `<mj-image src="${html(attrs(node).image)}" alt="${html(attrs(node).title)}" /><mj-text font-size="22px" font-weight="700">${html(attrs(node).title)}</mj-text><mj-text>${html(attrs(node).price)}</mj-text><mj-button href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</mj-button>`,
|
||||
emailHtml: ({ node }) => emailCell(`${marketingImage(node, "html")}<h2 style="font-family:inherit;">${html(attrs(node).title)}</h2><p style="font-family:inherit;font-weight:700;">${html(attrs(node).price)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
||||
ampEmail: ({ node }) => emailCell(`${marketingImage(node, "amp")}<h2>${html(attrs(node).title)}</h2><p>${html(attrs(node).price)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
||||
mjml: ({ node }) => `${marketingImage(node, "mjml")}<mj-text font-size="22px" font-weight="700">${html(attrs(node).title)}</mj-text><mj-text>${html(attrs(node).price)}</mj-text><mj-button href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</mj-button>`,
|
||||
},
|
||||
({ node }) => `${String(attrs(node).title)} — ${String(attrs(node).price)}\n${String(attrs(node).buttonLabel)}: ${String(attrs(node).href)}\n\n`,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user