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`,
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ const head = (document: BlockDocument, body: string, direction: "ltr" | "rtl") =
|
||||
};
|
||||
const ampHead = (document: BlockDocument, body: string, direction: "ltr" | "rtl") => {
|
||||
const props = document.root.props as Record<string, unknown>;
|
||||
const css = `body{margin:0;color:${props.textColor ?? "#111827"};font-family:${props.fontFamily ?? "Arial, sans-serif"}}a{color:${props.linkColor ?? "#2563eb"}}.email-bg{background:${props.background ?? "#f3f4f6"}}.email-body{max-width:${props.width ?? "600"}px;width:100%;background:${props.contentBackground ?? "#ffffff"}}`;
|
||||
const css = `.marketing-image img{object-fit:contain}body{margin:0;color:${props.textColor ?? "#111827"};font-family:${props.fontFamily ?? "Arial, sans-serif"}}a{color:${props.linkColor ?? "#2563eb"}}.email-bg{background:${props.background ?? "#f3f4f6"}}.email-body{max-width:${props.width ?? "600"}px;width:100%;background:${props.contentBackground ?? "#ffffff"}}`;
|
||||
return `<!doctype html><html amp4email data-css-strict lang="${escapeHtml(document.meta.lang)}" dir="${direction}"><head><meta charset="utf-8"><script async src="https://cdn.ampproject.org/v0.js"></script><style amp4email-boilerplate>body{visibility:hidden}</style><style amp-custom>${css}.section{padding:24px}.button{background:#2563eb;border-radius:6px;color:#fff;display:inline-block;font-weight:700;padding:12px 20px;text-decoration:none}.spacer{height:24px}</style></head><body>${body}</body></html>`;
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user