Skip to main content

Transactional email, native to Nuxt.

Author typed Vue SFCs with Tailwind utilities, then render deterministic HTML, plain text, and subject lines from the same Nitro-native path.

app/emails/welcome.vue
<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>

Typed, name-checked rendering

renderEmail('welcome', props) is generated from app/emails/. Unknown names, missing props, and wrong types fail at compile time.

Tailwind v4, inlined

ETailwind compiles utilities into inline styles and preserves the head rules email clients need, including classes from nested Vue components.

Deterministic HTML and plain text

Two renders of the same template and props return byte-identical HTML and a matching plain-text fallback. No IDs, timestamps, or hydration payload.

Outlook-safe primitives

Eighteen built-in E* components handle email structure and styling; configured applications can add syntax-highlighted ECodeBlock.

Development preview

Open /__email in dev to inspect preview, exact HTML, and plain text with sibling fixtures. Preview routes and fixtures never reach production.

Conformance with evidence

The ported component behaviors are checked against a pinned React Email 6.9.0 oracle, with intentional differences recorded in a generated report.