Skip to main content

renderEmail

The one public render API — its signature, deterministic result shape, plain-text contract, and security boundary.

renderEmail(name, props) is the one public application rendering path. Nuxt generates template names and prop types from the Vue single-file components (.vue files) in app/emails/. It resolves the selected component through the same server-only registry used by the development preview.

ts
const result = await renderEmail('welcome', {
  firstName: 'Ada',
  activationUrl: 'https://example.com/activate',
})

It is a Nitro auto-import — available in server handlers and other Nitro server code, not in Vue components, client plugins, or production client bundles. Do not import it from @lupinum/nuxt-email or #imports.

Result shape

The promise resolves to the canonical RenderedEmail type, available from the package root as a type-only import:

ts
import type { RenderedEmail } from '@lupinum/nuxt-email'
  • html — the rendered document. Begins with the XHTML 1.0 Transitional doctype.
  • text — authored metadata when declared, otherwise the derived plain-text fallback (see below).
  • subject — present only when the template declares one via defineEmail; otherwise absent.

There is no preview metadata, provider result, diagnostics object, timing, or size field. Sending is not part of this function — your application supplies recipients, sender identity, subject, credentials, and provider-specific options directly to its chosen provider SDK.

HTML contract

  • A template must render exactly one <html> root containing exactly one <body>. EHtml requires an explicit non-empty language and defaults only dir="ltr"; the renderer never repairs or auto-wraps fragments, text roots, body-only templates, or multiple roots.
  • EHead is optional but supplies the recommended UTF-8 and Apple reformatting meta tags.
  • Vue SSR interpolation and attribute values are escaped.
  • A fresh isolated Vue SSR application is created for every call. Email templates receive E-components through server-only auto-imports.
  • Framework code never fetches images, stylesheets, fonts, or other remote assets during rendering.

Determinism: two calls with the same template code and props return byte-identical html and text. Templates remain responsible for avoiding clocks, random values, mutable external state, and nondeterministic data.

Props and generated types

Template names are relative .vue paths under app/emails/, without the extension and with / separators — app/emails/account/reset-password.vue becomes account/reset-password. Files under app/emails/components/ are excluded.

Props declared with ordinary defineProps() or Vue's props option drive both the generated call-site type and runtime validation. TypeScript rejects unknown template names, missing required props, wrong values, and extra props. Untyped runtime calls reject unknown or missing props deterministically before SSR.

There is no public registry API — the generated registry is an internal server artifact and the sole source for rendering, generated types, and preview listings.

Select a template dynamically

Keep each template name paired with its props in a literal dispatch map. Validate external input before choosing an entry. Do not cast an arbitrary string to a template name; that removes the generated type protection.

server/utils/render-transactional-email.ts
interface WelcomeInput {
  firstName: string
  activationUrl: string
}

interface PasswordResetInput {
  code: string
  expiresInMinutes: number
}

export const emailDispatch = {
  welcome: (input: WelcomeInput) => renderEmail('welcome', input),
  passwordReset: (input: PasswordResetInput) => renderEmail('account/reset-password', input),
} as const

// Select an entry only after validating the event and its payload.
await emailDispatch.welcome({
  firstName: 'Ada',
  activationUrl: 'https://example.com/activate',
})

Each map entry contains a literal registry name. TypeScript checks its input against that template's generated props. A wrong name or prop shape fails where the entry is declared.

Plain-text contract

Unless the template declares authored text with defineEmail, Nuxt Email derives text from the final HTML. It uses a fixed html-to-text version with no line wrapping.

The conversion excludes head content, images, script and style content, and elements marked data-skip-in-text="true". This removes EPreview from the fallback. Links retain destinations when their visible text differs. See the plain-text guide for details and the data-text-format="dataTable" opt-in.

Security boundary

Nuxt Email renders trusted application templates with untrusted values supplied only through normal escaped Vue bindings. It is not an HTML sanitizer.

  • All E-prefixed primitives reject innerHTML, textContent, and attributes beginning with on.
  • There is no raw-HTML component. Do not use v-html or native raw HTML with untrusted content inside a template.
  • href and src values are escaped but URL schemes are not validated. Validate application-controlled URLs before rendering.
  • When consumed through the canonical generated API, template modules and the renderer are server-only and excluded from the client build. Do not import email templates into client code.
  • Development fixture modules, preview UI, and preview endpoints are absent from production builds. Do not import fixture modules into production code.
  • No production HTTP render route and no send route are generated. A route exists only when you create one.

The production server intentionally contains templates referenced by its registry. The guarantee is client exclusion, not removal from the server that renders them.

For render and lookup failures, see error types.