Typed, name-checked rendering
renderEmail('welcome', props) is generated from app/emails/. Unknown names, missing props, and wrong types fail at compile time.
Author typed Vue SFCs with Tailwind utilities, then render deterministic HTML, plain text, and subject lines from the same Nitro-native path.
<script setup lang="ts">
import { defineEmail } from '@lupinum/nuxt-email/define-email'
const props = defineProps<{ firstName: string; activationUrl: string }>()
defineEmail({ subject: () => `Welcome, ${props.firstName}` })
</script>
<template>
<ETailwind>
<EHtml lang="en">
<EHead><title>Activate your account</title></EHead>
<EBody class="bg-slate-100 p-6">
<EPreview>Your account is ready.</EPreview>
<EContainer class="rounded-xl bg-white p-8">
<EHeading>Welcome, {{ props.firstName }}</EHeading>
<EButton :href="props.activationUrl" class="bg-blue-600 px-5 py-3 text-white">
Activate account
</EButton>
</EContainer>
</EBody>
</EHtml>
</ETailwind>
</template>renderEmail('welcome', props) is generated from app/emails/. Unknown names, missing props, and wrong types fail at compile time.
ETailwind compiles utilities into inline styles and preserves the head rules email clients need, including classes from nested Vue components.
Two renders of the same template and props return byte-identical HTML and a matching plain-text fallback. No IDs, timestamps, or hydration payload.
Eighteen built-in E* components handle email structure and styling; configured applications can add syntax-highlighted ECodeBlock.
Open /__email in dev to inspect preview, exact HTML, and plain text with sibling fixtures. Preview routes and fixtures never reach production.
The ported component behaviors are checked against a pinned React Email 6.9.0 oracle, with intentional differences recorded in a generated report.