fix(email-renderer): inherit global brand fonts in content blocks
This commit is contained in:
1 parent
2cd8f8ea14
commit
1143da808f
3 files changed
+20
-9
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/email-renderer",
|
"name": "@wrnexus/email-renderer",
|
||||||
"version": "0.1.8",
|
"version": "0.1.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
"description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export const headingBlock = leaf(
|
|||||||
{
|
{
|
||||||
emailHtml: ({ node }) =>
|
emailHtml: ({ node }) =>
|
||||||
emailCell(
|
emailCell(
|
||||||
`<h${attrs(node).level} style="font-family:Arial,sans-serif;margin:0 0 16px;">${html(attrs(node).text)}</h${attrs(node).level}>`,
|
`<h${attrs(node).level} style="font-family:inherit;margin:0 0 16px;">${html(attrs(node).text)}</h${attrs(node).level}>`,
|
||||||
node,
|
node,
|
||||||
),
|
),
|
||||||
ampEmail: ({ node }) =>
|
ampEmail: ({ node }) =>
|
||||||
@@ -270,7 +270,7 @@ export const textBlock = leaf(
|
|||||||
{
|
{
|
||||||
emailHtml: ({ node }) =>
|
emailHtml: ({ node }) =>
|
||||||
emailCell(
|
emailCell(
|
||||||
`<p style="font-family:Arial,sans-serif;line-height:1.5;margin:0 0 16px;white-space:pre-line;">${html(attrs(node).text)}</p>`,
|
`<p style="font-family:inherit;line-height:1.5;margin:0 0 16px;white-space:pre-line;">${html(attrs(node).text)}</p>`,
|
||||||
node,
|
node,
|
||||||
),
|
),
|
||||||
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).text)}</p>`, node),
|
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).text)}</p>`, node),
|
||||||
@@ -318,7 +318,7 @@ export const buttonBlock = leaf(
|
|||||||
{
|
{
|
||||||
emailHtml: ({ node }) =>
|
emailHtml: ({ node }) =>
|
||||||
emailCell(
|
emailCell(
|
||||||
`<table role="presentation"><tr><td bgcolor="#2563eb" style="border-radius:6px;"><a href="${html(attrs(node).href)}" style="color:#ffffff;display:inline-block;font-family:Arial,sans-serif;font-weight:700;padding:12px 20px;text-decoration:none;">${html(attrs(node).label)}</a></td></tr></table>`,
|
`<table role="presentation"><tr><td bgcolor="#2563eb" style="border-radius:6px;"><a href="${html(attrs(node).href)}" style="color:#ffffff;display:inline-block;font-family:inherit;font-weight:700;padding:12px 20px;text-decoration:none;">${html(attrs(node).label)}</a></td></tr></table>`,
|
||||||
node,
|
node,
|
||||||
),
|
),
|
||||||
ampEmail: ({ node }) =>
|
ampEmail: ({ node }) =>
|
||||||
@@ -394,7 +394,7 @@ export const socialBlock = leaf(
|
|||||||
x: safeUrl({ schemes: ["https"], optional: true }),
|
x: safeUrl({ schemes: ["https"], optional: true }),
|
||||||
}) as never,
|
}) as never,
|
||||||
{
|
{
|
||||||
emailHtml: ({ node }) => emailCell(`<p style="font-family:Arial,sans-serif;margin:0 0 16px;">${html(attrs(node).label)}: ${[["Facebook",attrs(node).facebook],["Instagram",attrs(node).instagram],["LinkedIn",attrs(node).linkedin],["X",attrs(node).x]].filter(([,url])=>url).map(([name,url])=>`<a href="${html(url)}" style="margin-right:12px;">${name}</a>`).join("")}</p>`, node),
|
emailHtml: ({ node }) => emailCell(`<p style="font-family:inherit;margin:0 0 16px;">${html(attrs(node).label)}: ${[["Facebook",attrs(node).facebook],["Instagram",attrs(node).instagram],["LinkedIn",attrs(node).linkedin],["X",attrs(node).x]].filter(([,url])=>url).map(([name,url])=>`<a href="${html(url)}" style="margin-right:12px;">${name}</a>`).join("")}</p>`, node),
|
||||||
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).label)}: ${[["Facebook",attrs(node).facebook],["Instagram",attrs(node).instagram],["LinkedIn",attrs(node).linkedin],["X",attrs(node).x]].filter(([,url])=>url).map(([name,url])=>`<a href="${html(url)}">${name}</a>`).join(" ")}</p>`, node),
|
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).label)}: ${[["Facebook",attrs(node).facebook],["Instagram",attrs(node).instagram],["LinkedIn",attrs(node).linkedin],["X",attrs(node).x]].filter(([,url])=>url).map(([name,url])=>`<a href="${html(url)}">${name}</a>`).join(" ")}</p>`, node),
|
||||||
mjml: ({ node }) => `<mj-text>${html(attrs(node).label)}: ${[["Facebook",attrs(node).facebook],["Instagram",attrs(node).instagram],["LinkedIn",attrs(node).linkedin],["X",attrs(node).x]].filter(([,url])=>url).map(([name,url])=>`<a href="${html(url)}">${name}</a>`).join(" ")}</mj-text>`,
|
mjml: ({ node }) => `<mj-text>${html(attrs(node).label)}: ${[["Facebook",attrs(node).facebook],["Instagram",attrs(node).instagram],["LinkedIn",attrs(node).linkedin],["X",attrs(node).x]].filter(([,url])=>url).map(([name,url])=>`<a href="${html(url)}">${name}</a>`).join(" ")}</mj-text>`,
|
||||||
},
|
},
|
||||||
@@ -411,7 +411,7 @@ export const countdownBlock = leaf(
|
|||||||
fallback: string({ max: 300, default: "This offer has ended." }),
|
fallback: string({ max: 300, default: "This offer has ended." }),
|
||||||
}) as never,
|
}) as never,
|
||||||
{
|
{
|
||||||
emailHtml: ({ node }) => emailCell(`<p style="font-family:Arial,sans-serif;font-size:20px;font-weight:700;margin:0 0 16px;">${html(attrs(node).label)}: <time datetime="${html(attrs(node).deadline)}">${html(attrs(node).deadline)}</time></p>`, node),
|
emailHtml: ({ node }) => emailCell(`<p style="font-family:inherit;font-size:20px;font-weight:700;margin:0 0 16px;">${html(attrs(node).label)}: <time datetime="${html(attrs(node).deadline)}">${html(attrs(node).deadline)}</time></p>`, node),
|
||||||
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).label)}: <time datetime="${html(attrs(node).deadline)}">${html(attrs(node).deadline)}</time></p>`, node),
|
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).label)}: <time datetime="${html(attrs(node).deadline)}">${html(attrs(node).deadline)}</time></p>`, node),
|
||||||
mjml: ({ node }) => `<mj-text font-size="20px" font-weight="700">${html(attrs(node).label)}: ${html(attrs(node).deadline)}</mj-text>`,
|
mjml: ({ node }) => `<mj-text font-size="20px" font-weight="700">${html(attrs(node).label)}: ${html(attrs(node).deadline)}</mj-text>`,
|
||||||
},
|
},
|
||||||
@@ -424,7 +424,7 @@ export const surveyBlock = leaf(
|
|||||||
"icon-[lucide--list-checks]",
|
"icon-[lucide--list-checks]",
|
||||||
object({ question: string({ min: 1, max: 500 }), buttonLabel: string({ min: 1, max: 200, default: "Answer survey" }), href: safeUrl({ schemes: ["https"] }) }) as never,
|
object({ question: string({ min: 1, max: 500 }), buttonLabel: string({ min: 1, max: 200, default: "Answer survey" }), href: safeUrl({ schemes: ["https"] }) }) as never,
|
||||||
{
|
{
|
||||||
emailHtml: ({ node }) => emailCell(`<p style="font-family:Arial,sans-serif;margin:0 0 12px;">${html(attrs(node).question)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
emailHtml: ({ node }) => emailCell(`<p style="font-family:inherit;margin:0 0 12px;">${html(attrs(node).question)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
||||||
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).question)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
ampEmail: ({ node }) => emailCell(`<p>${html(attrs(node).question)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
||||||
mjml: ({ node }) => `<mj-text>${html(attrs(node).question)}</mj-text><mj-button href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</mj-button>`,
|
mjml: ({ node }) => `<mj-text>${html(attrs(node).question)}</mj-text><mj-button href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</mj-button>`,
|
||||||
},
|
},
|
||||||
@@ -437,7 +437,7 @@ export const feedItemBlock = leaf(
|
|||||||
"icon-[lucide--rss]",
|
"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({ 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:Arial,sans-serif;"><a href="${html(attrs(node).href)}">${html(attrs(node).title)}</a></h2><p style="font-family:Arial,sans-serif;">${html(attrs(node).summary)}</p>`, node),
|
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),
|
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>`,
|
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>`,
|
||||||
},
|
},
|
||||||
@@ -450,7 +450,7 @@ export const productBlock = leaf(
|
|||||||
"icon-[lucide--shopping-bag]",
|
"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({ 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:Arial,sans-serif;">${html(attrs(node).title)}</h2><p style="font-family:Arial,sans-serif;font-weight:700;">${html(attrs(node).price)}</p><p><a href="${html(attrs(node).href)}">${html(attrs(node).buttonLabel)}</a></p>`, node),
|
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),
|
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>`,
|
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>`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -56,6 +56,17 @@ const document: BlockDocument = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("email renderer", () => {
|
describe("email renderer", () => {
|
||||||
|
test("content inherits the selected brand font instead of overriding it", async () => {
|
||||||
|
const branded = structuredClone(document);
|
||||||
|
branded.root.props.fontFamily = "Georgia, serif";
|
||||||
|
const result = await renderEmail(branded);
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) return;
|
||||||
|
expect(result.value.parts.html.content).toContain("font-family:Georgia, serif");
|
||||||
|
expect(result.value.parts.html.content).not.toContain("font-family:Arial");
|
||||||
|
expect(result.value.parts.html.content).toContain('<h1 style="font-family:inherit');
|
||||||
|
expect(result.value.parts.html.content).toContain("display:inline-block;font-family:inherit");
|
||||||
|
});
|
||||||
test("browser HTML parsing preserves every block inside its section", async () => {
|
test("browser HTML parsing preserves every block inside its section", async () => {
|
||||||
const rendered = await renderEmail(document);
|
const rendered = await renderEmail(document);
|
||||||
expect(rendered.ok).toBe(true);
|
expect(rendered.ok).toBe(true);
|
||||||
|
|||||||
Reference in new issue
Block a user