diff --git a/packages/demo/src/App.tsx b/packages/demo/src/App.tsx index 967227d3..b5a04528 100644 --- a/packages/demo/src/App.tsx +++ b/packages/demo/src/App.tsx @@ -3,6 +3,7 @@ import Hero from "./sections/Hero"; import QuickStart from "./sections/QuickStart"; import TemplateShowcase from "./sections/TemplateShowcase"; import RenderModeDemo from "./sections/RenderModeDemo"; +import NewsletterArchiveDemo from "./sections/NewsletterArchiveDemo"; import FeatureComparison from "./sections/FeatureComparison"; import ComponentGallery from "./sections/ComponentGallery"; import CTABanner from "./sections/CTABanner"; @@ -43,6 +44,7 @@ export default function App() {
+
diff --git a/packages/demo/src/hooks/useRenderTemplate.ts b/packages/demo/src/hooks/useRenderTemplate.ts index afffae97..bbca3942 100644 --- a/packages/demo/src/hooks/useRenderTemplate.ts +++ b/packages/demo/src/hooks/useRenderTemplate.ts @@ -23,7 +23,7 @@ export function useRenderTemplate(templateId: string): RenderResult { let plainText = ""; try { - const parts = renderToHtmlParts(element, { mode: "email" }); + const parts = renderToHtmlParts(element, { mode: entry.mode ?? "email" }); head = parts.head; html = parts.body; } catch (e) { diff --git a/packages/demo/src/sections/NewsletterArchiveDemo.tsx b/packages/demo/src/sections/NewsletterArchiveDemo.tsx new file mode 100644 index 00000000..6f6c3b0f --- /dev/null +++ b/packages/demo/src/sections/NewsletterArchiveDemo.tsx @@ -0,0 +1,97 @@ +import { useMemo } from "react"; +import { renderToHtmlParts } from "@unlayer/react-elements"; +import { newsletterTemplate, type NewsletterOutput } from "../templates/NewsletterDigest"; +import SectionHeader from "../components/SectionHeader"; +import DeviceFrame from "../components/DeviceFrame"; +import CodeBlock from "../components/CodeBlock"; + +const snippet = `// One tree, two outputs — only the wrapper changes. +function newsletterTemplate(output: "email" | "web") { + const Wrapper = output === "web" ? Page : Email; + return ( + + {/* ...masthead, featured article, article list, footer... */} + + ); +} + +// Send this: +const email = renderToHtml(newsletterTemplate("email")); // tables, Outlook-safe +// Publish this at /archive/issue-47: +const page = renderToHtml(newsletterTemplate("web")); // div + flexbox`; + +const outputs: { id: NewsletterOutput; wrapper: string; label: string; blurb: string }[] = [ + { + id: "email", + wrapper: "Email", + label: "In the inbox", + blurb: "Nested tables with inlined styles — what Outlook and Gmail need.", + }, + { + id: "web", + wrapper: "Page", + label: "In the browser", + blurb: "Semantic divs and flexbox — the archive page you link from “View in browser”.", + }, +]; + +export default function NewsletterArchiveDemo() { + const rendered = useMemo( + () => + outputs.map((output) => { + try { + const { head, body } = renderToHtmlParts(newsletterTemplate(output.id), { + mode: output.id, + }); + return { ...output, head, body }; + } catch (e) { + const message = e instanceof Error ? e.message : String(e); + return { ...output, head: "", body: `` }; + } + }), + [] + ); + + return ( +
+ {/* Background */} +
+
+ +
+ + +
+
+ {rendered.map((output) => ( +
+
+ + {`<${output.wrapper}>`} + + {output.label} +
+

{output.blurb}

+ +
+ ))} +
+ +
+ +

+ No second template to keep in sync. Swap {""} for{" "} + {""} and the same content ships to both the inbox + and the web — see the Newsletter Web Archive entry above + for the full source. +

+
+
+
+
+ ); +} diff --git a/packages/demo/src/templates/NewsletterDigest.tsx b/packages/demo/src/templates/NewsletterDigest.tsx index 4ebba1cd..ac2ab6b8 100644 --- a/packages/demo/src/templates/NewsletterDigest.tsx +++ b/packages/demo/src/templates/NewsletterDigest.tsx @@ -1,6 +1,7 @@ import type { ReactElement } from "react"; import { Email, + Page, Row, Column, Paragraph, @@ -21,9 +22,24 @@ const uiFont = { value: "system-ui, -apple-system, BlinkMacSystemFont, sans-serif", }; -export default function NewsletterDigest(): ReactElement { +/** Which output this newsletter is being rendered for. */ +export type NewsletterOutput = "email" | "web"; + +/** + * The newsletter, wrapper-agnostic. + * + * Everything inside the wrapper is identical for both outputs — only the root + * component changes: + * → table-based HTML for inboxes + * → div/flexbox HTML for the browser ("view in browser" archive) + * + * See NewsletterWebArchive.tsx for the web variant. + */ +export function newsletterTemplate(output: NewsletterOutput): ReactElement { + const Wrapper = output === "web" ? Page : Email; + return ( - + {/* The only content that differs between the two outputs: the inbox + copy links out to the archive, the archive links back to signup. */} Subscribe · Browse the archive · RSS' + : 'Unsubscribe · View in browser · Update preferences' + } fontSize="12px" color="#999999" textAlign="center" @@ -288,6 +310,11 @@ export default function NewsletterDigest(): ReactElement { /> - + ); } + +/** The newsletter as an email — table-based HTML for inboxes. */ +export default function NewsletterDigest(): ReactElement { + return newsletterTemplate("email"); +} diff --git a/packages/demo/src/templates/NewsletterWebArchive.tsx b/packages/demo/src/templates/NewsletterWebArchive.tsx new file mode 100644 index 00000000..1326b8ea --- /dev/null +++ b/packages/demo/src/templates/NewsletterWebArchive.tsx @@ -0,0 +1,22 @@ +import type { ReactElement } from "react"; +import { newsletterTemplate } from "./NewsletterDigest"; + +/** + * Newsletter web archive — the "view in browser" page for the newsletter email. + * + * This is the *same component tree* as NewsletterDigest. Nothing is duplicated: + * `newsletterTemplate()` swaps only the root wrapper. + * + * newsletterTemplate("email") → → tables, inlined for Outlook/Gmail + * newsletterTemplate("web") → → div/flexbox, responsive browser HTML + * + * The rows, columns, headings, images, and buttons in between are byte-identical. + * That is the point: send the email, then publish the archive page from the same + * source — no second template to keep in sync. + * + * Rendering it is the usual one-liner: + * renderToHtml() // or renderToHtmlParts() to own the shell + */ +export default function NewsletterWebArchive(): ReactElement { + return newsletterTemplate("web"); +} diff --git a/packages/demo/src/templates/index.ts b/packages/demo/src/templates/index.ts index e39035fe..35495af0 100644 --- a/packages/demo/src/templates/index.ts +++ b/packages/demo/src/templates/index.ts @@ -7,6 +7,7 @@ import OrderConfirmation from "./OrderConfirmation"; import ReviewRequest from "./ReviewRequest"; import PasswordReset from "./PasswordReset"; import NewsletterDigest from "./NewsletterDigest"; +import NewsletterWebArchive from "./NewsletterWebArchive"; import AbandonedCart from "./AbandonedCart"; import ShippingUpdate from "./ShippingUpdate"; import ProductLaunch from "./ProductLaunch"; @@ -21,6 +22,7 @@ import orderSource from "./OrderConfirmation.tsx?raw"; import reviewSource from "./ReviewRequest.tsx?raw"; import passwordSource from "./PasswordReset.tsx?raw"; import newsletterSource from "./NewsletterDigest.tsx?raw"; +import newsletterArchiveSource from "./NewsletterWebArchive.tsx?raw"; import abandonedCartSource from "./AbandonedCart.tsx?raw"; import shippingSource from "./ShippingUpdate.tsx?raw"; import productLaunchSource from "./ProductLaunch.tsx?raw"; @@ -101,6 +103,17 @@ export const templates: TemplateEntry[] = [ component: NewsletterDigest, sourceCode: newsletterSource, }, + { + id: "newsletter-web-archive", + name: "Newsletter Web Archive", + description: "The same newsletter tree rendered with as a browser archive page", + category: "newsletter", + inspiration: "Substack", + colorAccent: "#e85d04", + component: NewsletterWebArchive, + sourceCode: newsletterArchiveSource, + mode: "web", + }, { id: "abandoned-cart", name: "Abandoned Cart",