From cf9e486367d41dde367d590dc2a03db49a07f79d Mon Sep 17 00:00:00 2001 From: rajashidattapy Date: Mon, 27 Jul 2026 16:27:23 +0530 Subject: [PATCH] feat(examples): add runnable Next.js App Router example (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renders an Elements email to HTML inside a Next.js 15 App Router Server Component and previews it at /email-preview. - examples/nextjs-app-router/ — welcome email component tree, a Server Component calling renderToHtml() + renderToPlainText() into an iframe preview, and a Route Handler serving the raw HTML as text/html - README with setup instructions, linked from a new Examples section in the root README The example installs @unlayer/react-elements from npm rather than via workspace:*. A symlinked workspace copy resolves outside node_modules, so Next compiles it as first-party source and its RSC checks reject the package's react-dom/server import and the provider's createContext — `next dev` 500s and `next build` fails. serverExternalPackages does not help; that list only matches node_modules resolutions. Installing from npm lets Next externalise it on the server like any other dependency. That required excluding the example from the workspace and giving it its own pnpm-workspace.yaml, so `pnpm install` inside the directory installs the example alone. Coverage against the local build is unchanged — tests/nextjs-integration still packs the built tarball and runs next build in CI. --- .gitignore | 2 + README.md | 8 + examples/nextjs-app-router/.gitignore | 4 + examples/nextjs-app-router/README.md | 90 ++ examples/nextjs-app-router/next-env.d.ts | 6 + examples/nextjs-app-router/next.config.ts | 5 + examples/nextjs-app-router/package.json | 23 + examples/nextjs-app-router/pnpm-lock.yaml | 923 ++++++++++++++++++ .../nextjs-app-router/pnpm-workspace.yaml | 4 + .../src/app/email-preview/page.tsx | 83 ++ .../src/app/email-preview/raw/route.tsx | 20 + examples/nextjs-app-router/src/app/layout.tsx | 24 + examples/nextjs-app-router/src/app/page.tsx | 26 + .../src/emails/welcome-email.tsx | 172 ++++ examples/nextjs-app-router/tsconfig.json | 21 + pnpm-workspace.yaml | 6 + 16 files changed, 1417 insertions(+) create mode 100644 examples/nextjs-app-router/.gitignore create mode 100644 examples/nextjs-app-router/README.md create mode 100644 examples/nextjs-app-router/next-env.d.ts create mode 100644 examples/nextjs-app-router/next.config.ts create mode 100644 examples/nextjs-app-router/package.json create mode 100644 examples/nextjs-app-router/pnpm-lock.yaml create mode 100644 examples/nextjs-app-router/pnpm-workspace.yaml create mode 100644 examples/nextjs-app-router/src/app/email-preview/page.tsx create mode 100644 examples/nextjs-app-router/src/app/email-preview/raw/route.tsx create mode 100644 examples/nextjs-app-router/src/app/layout.tsx create mode 100644 examples/nextjs-app-router/src/app/page.tsx create mode 100644 examples/nextjs-app-router/src/emails/welcome-email.tsx create mode 100644 examples/nextjs-app-router/tsconfig.json diff --git a/.gitignore b/.gitignore index 388d50c0..00885de9 100644 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,5 @@ tsup.config.bundled_*.mjs # Claude Code .claude/ + +__snapshots__/ \ No newline at end of file diff --git a/README.md b/README.md index 0e941e69..0febf538 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,14 @@ For the full API reference, component props, design patterns, and common mistake | [`@unlayer-internal/shared-elements`](./packages/shared) | Framework-agnostic shared logic | Internal | | [`@unlayer/elements-demo`](./packages/demo) | Demo application | — | +## Examples + +Runnable projects you can clone and experiment with: + +| Example | Description | +|---------|-------------| +| [`examples/nextjs-app-router`](./examples/nextjs-app-router) | Next.js 15 App Router — renders an email with `renderToHtml()` in a Server Component, previews it at `/email-preview`, and serves the raw HTML from a Route Handler | + ## Development ```bash diff --git a/examples/nextjs-app-router/.gitignore b/examples/nextjs-app-router/.gitignore new file mode 100644 index 00000000..c3a54e7a --- /dev/null +++ b/examples/nextjs-app-router/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.next/ +package-lock.json +tsconfig.tsbuildinfo diff --git a/examples/nextjs-app-router/README.md b/examples/nextjs-app-router/README.md new file mode 100644 index 00000000..1962331d --- /dev/null +++ b/examples/nextjs-app-router/README.md @@ -0,0 +1,90 @@ +# Next.js App Router example + +Renders an Unlayer Elements email to HTML inside a Next.js 15 App Router +**Server Component**, then previews it in the browser. + +## Run it + +```bash +cd examples/nextjs-app-router +pnpm install +pnpm dev +``` + +Then open: + +- — the email rendered in a preview frame +- — the exact HTML string, served as `text/html` + +This example is self-contained: it installs `@unlayer/react-elements` from npm +like any other app, so nothing in the monorepo needs to be built first. Copy the +directory anywhere and it still runs. + +## What it shows + +| File | Shows | +|------|-------| +| `src/emails/welcome-email.tsx` | A realistic transactional email as a plain component tree, parameterised by props | +| `src/app/email-preview/page.tsx` | A Server Component calling `renderToHtml()` and `renderToPlainText()`, previewing the result in an iframe | +| `src/app/email-preview/raw/route.tsx` | A Route Handler returning the rendered HTML — the shape production code usually takes | + +The key point is what is **absent**: no `"use client"`, no `useEffect`, no +client-side rendering library. `renderToHtml()` runs during the server render +and only the resulting string reaches the browser. + +```tsx +// src/app/email-preview/page.tsx — Server Component +import { renderToHtml } from "@unlayer/react-elements"; +import WelcomeEmail from "@/emails/welcome-email"; + +export default function EmailPreviewPage() { + const html = renderToHtml(, { + title: "Welcome, Ada", + }); + + return