diff --git a/packages/email-renderer/package.json b/packages/email-renderer/package.json index f35a8259..decea1c9 100644 --- a/packages/email-renderer/package.json +++ b/packages/email-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/email-renderer", - "version": "0.1.29", + "version": "0.1.30", "type": "module", "description": "WRNexus block email renderers with HTML, text, AMP Email, and MJML interoperability.", "exports": { diff --git a/packages/email-renderer/src/blocks.ts b/packages/email-renderer/src/blocks.ts index f3e7cc5e..091dcd56 100644 --- a/packages/email-renderer/src/blocks.ts +++ b/packages/email-renderer/src/blocks.ts @@ -34,6 +34,10 @@ const html = (value: unknown) => const attrs = (node: BlockNode) => node.props as Record; const isEmailRootChild = (node: BlockNode, context: unknown) => (context as { rootBlockIds?: string[] } | undefined)?.rootBlockIds?.includes(node.id) ?? false; +const mjmlDocumentHead = (node: BlockNode, context: unknown) => { + const meta = context as { emailSubject?: string; emailPreheader?: string } | undefined; + return `${html(meta?.emailSubject)}${meta?.emailPreheader ? `${html(meta.emailPreheader)}` : ""}`; +}; export const ampLayoutClass = (node: BlockNode) => "wrn-layout-" + Array.from(node.id, char => char.codePointAt(0)!.toString(16)).join("-"); @@ -158,8 +162,8 @@ export const emailDocumentBlock = defineBlock({ `
${attrs(node).logoUrl ? `${html(attrs(node).logoAlt)}` : ""}${slots.content}
`, ampEmail: ({ node, slots }) => ``, - mjml: ({ node, slots }) => - `${attrs(node).logoUrl ? `` : ""}${slots.content}`, + mjml: ({ node, slots, context }) => + `${mjmlDocumentHead(node, context)}${attrs(node).logoUrl ? `` : ""}${slots.content}`, }, textRenderer: ({ slots }) => slots.content, } satisfies BlockDefinition); diff --git a/packages/email-renderer/src/email.ts b/packages/email-renderer/src/email.ts index 7db50ca3..c52f139b 100644 --- a/packages/email-renderer/src/email.ts +++ b/packages/email-renderer/src/email.ts @@ -140,6 +140,8 @@ export async function renderEmail( direction: options.direction ?? "ltr", countdownImageUrl: options.countdownImageUrl, brandLinkColor: String(source.root.props.linkColor ?? "#2563eb"), + emailSubject: String(source.meta.subject), + emailPreheader: String(source.meta.preheader ?? ""), rootBlockIds: Object.values(source.root.slots).flat().map(node => node.id), }; const [htmlResult, textResult, mjmlResult] = await Promise.all([ diff --git a/packages/email-renderer/test/mjml-brand.test.ts b/packages/email-renderer/test/mjml-brand.test.ts new file mode 100644 index 00000000..ad26147f --- /dev/null +++ b/packages/email-renderer/test/mjml-brand.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "bun:test"; +import { createRequire } from "node:module"; +import type { BlockDocument } from "@wrnexus/blocks"; +import { renderEmail } from "../src/index.ts"; + +const compile = createRequire(import.meta.url)("mjml"); + +test("MJML preserves document metadata and global typography in compiled HTML", async () => { + const document: BlockDocument = { + schemaVersion: 1, kind: "email", meta: { subject: 'Brand & "News" ', preheader: 'Preview & useful', lang: "en" }, theme: { tokens: {} }, + root: { id: "root", type: "layout.email-document", version: 1, props: { width: "600", fontFamily: "Georgia, serif", textColor: "#123456" }, styles: {}, slots: { + content: [{ id: "text", type: "content.text", version: 1, props: { text: "Brand text" }, styles: {}, slots: {} }, { id: "button", type: "content.button", version: 1, props: { label: "Brand action", href: "https://example.com" }, styles: {}, slots: {} }], + } }, + }; + const before = structuredClone(document); + const result = await renderEmail(document, { mjml: true }); + expect(result.ok).toBe(true); + if (!result.ok) return; + const source = result.value.exports!.mjml!; + expect(source).toContain('Brand & "News" <today>'); + expect(source).toContain('Preview <safe> & useful'); + expect(source).toContain(''); + const compiled = await compile(source, { validationLevel: "strict", minify: false }); + expect(compiled.errors).toEqual([]); + expect(compiled.html).toContain("font-family:Georgia, serif"); + expect(compiled.html).toContain("color:#123456"); + expect(compiled.html).toContain("Preview <safe> & useful"); + expect(compiled.html).not.toContain(""); + expect(document).toEqual(before); +});