@lupinum/nuxt-email 1.0.0-beta.2 → 1.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +31 -3
  3. package/dist/agent/AGENTS.md +47 -0
  4. package/dist/agent/manifest.json +208 -0
  5. package/dist/agent/pages/docs/components/code.md +68 -0
  6. package/dist/agent/pages/docs/components/content.md +111 -0
  7. package/dist/agent/pages/docs/components/layout.md +89 -0
  8. package/dist/agent/pages/docs/components/markdown.md +49 -0
  9. package/dist/agent/pages/docs/components/structure.md +122 -0
  10. package/dist/agent/pages/docs/components/tailwind.md +41 -0
  11. package/dist/agent/pages/docs/components.md +56 -0
  12. package/dist/agent/pages/docs/getting-started/choose-a-tool.md +38 -0
  13. package/dist/agent/pages/docs/getting-started/first-email.md +99 -0
  14. package/dist/agent/pages/docs/getting-started/installation.md +97 -0
  15. package/dist/agent/pages/docs/getting-started/render-from-nitro.md +59 -0
  16. package/dist/agent/pages/docs/getting-started.md +62 -0
  17. package/dist/agent/pages/docs/guides/client-compatibility.md +65 -0
  18. package/dist/agent/pages/docs/guides/plain-text.md +42 -0
  19. package/dist/agent/pages/docs/guides/preview-workflow.md +83 -0
  20. package/dist/agent/pages/docs/guides/sending-email.md +132 -0
  21. package/dist/agent/pages/docs/guides/standalone-rendering.md +118 -0
  22. package/dist/agent/pages/docs/guides/subject-lines.md +95 -0
  23. package/dist/agent/pages/docs/guides/tailwind.md +128 -0
  24. package/dist/agent/pages/docs/guides/testing-your-emails.md +118 -0
  25. package/dist/agent/pages/docs/guides.md +32 -0
  26. package/dist/agent/pages/docs/reference/conformance.md +55 -0
  27. package/dist/agent/pages/docs/reference/define-email.md +69 -0
  28. package/dist/agent/pages/docs/reference/errors.md +72 -0
  29. package/dist/agent/pages/docs/reference/migration.md +144 -0
  30. package/dist/agent/pages/docs/reference/module.md +90 -0
  31. package/dist/agent/pages/docs/reference/render-email.md +107 -0
  32. package/dist/agent/pages/docs/reference.md +30 -0
  33. package/dist/module.json +1 -1
  34. package/dist/module.mjs +3 -3
  35. package/package.json +15 -9
@@ -0,0 +1,118 @@
1
+ ---
2
+ title: "Testing your emails"
3
+ description: "Render templates and assert on their output in unit tests with the @lupinum/nuxt-email/testing utilities."
4
+ url: "https://nuxt-email.lupinum.com/docs/guides/testing-your-emails"
5
+ route: "/docs/guides/testing-your-emails"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Testing your emails
13
+
14
+ > Render templates and assert on their output in unit tests with the @lupinum/nuxt-email/testing utilities.
15
+
16
+ Nuxt Email exposes a standalone renderer from `@lupinum/nuxt-email/testing`. It renders templates using the eighteen built-in components without booting a Nuxt app.
17
+
18
+ ```ts
19
+ import { renderEmailComponent } from '@lupinum/nuxt-email/testing'
20
+ ```
21
+
22
+ ## renderEmailComponent
23
+
24
+ Renders a Vue email component to `{ html, text, subject? }` — the same `RenderedEmail` shape [`renderEmail`](/raw/docs/reference/render-email.md) returns, produced by the same renderer.
25
+
26
+ ```ts [test/welcome.test.ts]
27
+ import { renderEmailComponent } from '@lupinum/nuxt-email/testing'
28
+ import Welcome from '../app/emails/welcome.vue'
29
+
30
+ test('welcome renders the recipient name', async () => {
31
+ const { html, text } = await renderEmailComponent(Welcome, {
32
+ firstName: 'Ada',
33
+ activationUrl: 'https://example.com/activate',
34
+ })
35
+
36
+ expect(html).toContain('Welcome, Ada')
37
+ expect(text).toContain('Welcome, Ada')
38
+ })
39
+ ```
40
+
41
+ Because this test imports a Vue single-file component (`.vue` file) directly, enable Vue's Vite transform in the test project:
42
+
43
+ ```ts [vitest.config.ts]
44
+ import vue from '@vitejs/plugin-vue'
45
+ import { defineConfig } from 'vitest/config'
46
+
47
+ export default defineConfig({
48
+ plugins: [vue()],
49
+ test: {
50
+ environment: 'node',
51
+ },
52
+ })
53
+ ```
54
+
55
+ Install `vitest` and `@vitejs/plugin-vue` as direct development dependencies. The helper is Node-only; do not import it from application components, shared browser code, or client plugins.
56
+
57
+ `renderEmailComponent` infers the second argument from the imported Vue component. Required props make it required, optional-only props make it optional, and prop-free components reject invented props. It returns `Promise<RenderedEmail>`. Because rendering is deterministic, the same component and props always produce the same output.
58
+
59
+ ## Testing configured components
60
+
61
+ When a template uses an application-configured component such as `ECodeBlock`, resolve the aliases generated by `nuxt prepare` and import the generated renderer binding:
62
+
63
+ ```ts [vitest.config.ts]
64
+ import vue from '@vitejs/plugin-vue'
65
+ import { readFileSync } from 'node:fs'
66
+ import { dirname, resolve } from 'node:path'
67
+ import { fileURLToPath } from 'node:url'
68
+ import { defineConfig } from 'vitest/config'
69
+
70
+ const tsconfigPath = fileURLToPath(new URL('./.nuxt/tsconfig.json', import.meta.url))
71
+ const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf8'))
72
+ const rendererPath = tsconfig.compilerOptions.paths['#nuxt-email/testing']?.[0]
73
+ if (!rendererPath) throw new Error('Run `nuxt prepare` before configured email tests')
74
+
75
+ export default defineConfig({
76
+ plugins: [vue()],
77
+ resolve: {
78
+ alias: {
79
+ '#nuxt-email/testing': resolve(dirname(tsconfigPath), rendererPath),
80
+ },
81
+ },
82
+ test: {
83
+ environment: 'node',
84
+ },
85
+ })
86
+ ```
87
+
88
+ Keep these configured-renderer tests under `test/nuxt/`, which Nuxt includes in that generated TypeScript project.
89
+
90
+ ```ts
91
+ import { renderEmailComponent } from '#nuxt-email/testing'
92
+ ```
93
+
94
+ Before running these tests:
95
+
96
+ 1. Install `vitest` and `@vitejs/plugin-vue`.
97
+ 2. Run `nuxt prepare` to generate the alias path.
98
+ 3. Keep configured-renderer tests under `test/nuxt/`.
99
+ 4. Import `#nuxt-email/testing` only from Node tests. Do not import it from application or shared client code.
100
+
101
+ The alias reads Nuxt's generated path as its source of truth. The Vue plugin compiles the `.vue` file without starting a browser application. Nuxt unit tests disable server-side rendering (SSR), but email components are server-only. The generated binding and production `renderEmail` use the same complete component registry.
102
+
103
+ The standalone package helper rejects unresolved `E*` components. It does not return fake custom-element markup in development or production.
104
+
105
+ Prefer focused assertions on recipient-visible text, links, required attributes, and client-specific markup. Whole-document snapshots can approve accidental output. The React and Vue comparison normalizer remains internal because its rules belong to the conformance suite, not the public HTML contract.
106
+
107
+ ## Asserting on the subject
108
+
109
+ If a template declares a subject with [`defineEmail`](/raw/docs/guides/subject-lines.md), `renderEmailComponent` computes it from the props you pass:
110
+
111
+ ```ts
112
+ const { subject } = await renderEmailComponent(Welcome, { firstName: 'Ada', activationUrl: '…' })
113
+ expect(subject).toBe('Welcome aboard, Ada')
114
+ ```
115
+
116
+ ## What testing does not replace
117
+
118
+ Unit tests prove your template's structure and content are stable and deterministic. They do **not** prove how a real client renders the message. Pair them with the [manual client compatibility](/raw/docs/guides/client-compatibility.md) checklist before a release.
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: "Guides"
3
+ description: "Task-focused guides for authoring, previewing, testing, and sending transactional email."
4
+ url: "https://nuxt-email.lupinum.com/docs/guides"
5
+ route: "/docs/guides"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Guides
13
+
14
+ > Task-focused guides for authoring, previewing, testing, and sending transactional email.
15
+
16
+ These guides go beyond the per-component reference to cover common authoring tasks end to end.
17
+
18
+ <cards cols="2">
19
+ <card title="Preview workflow" description="Use /__email and inspect the exact HTML, text, and subject." icon="lucide:eye" to="/docs/guides/preview-workflow" />
20
+
21
+ <card title="Sending email" description="Hand rendered HTML, text, and subject to a provider or local SMTP server." icon="lucide:send" to="/docs/guides/sending-email" />
22
+
23
+ <card title="Using Tailwind" description="Inline utilities, nested-component support, and documented limitations." icon="lucide:paintbrush" to="/docs/guides/tailwind" />
24
+
25
+ <card title="Subject lines" description="Compute a subject from props with defineEmail." icon="lucide:mail" to="/docs/guides/subject-lines" />
26
+
27
+ <card title="Plain text" description="How the text fallback is produced and how to control it." icon="lucide:file-text" to="/docs/guides/plain-text" />
28
+
29
+ <card title="Testing your emails" description="Render and assert with the scoped package testing utilities." icon="lucide:flask-conical" to="/docs/guides/testing-your-emails" />
30
+
31
+ <card title="Client compatibility" description="The manual QA checklist and what is proven." icon="lucide:check-check" to="/docs/guides/client-compatibility" />
32
+ </cards>
@@ -0,0 +1,55 @@
1
+ ---
2
+ title: "Conformance and divergences"
3
+ description: "How compatibility is measured against React Email, the current pass numbers, and every intentional divergence."
4
+ url: "https://nuxt-email.lupinum.com/docs/reference/conformance"
5
+ route: "/docs/reference/conformance"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Conformance and divergences
13
+
14
+ > How compatibility is measured against React Email, the current pass numbers, and every intentional divergence.
15
+
16
+ Compatibility is reported **per behavior**. No global "React Email compatible" percentage is claimed. The generated repository report is the authoritative source; this page explains its method.
17
+
18
+ ## How it is measured
19
+
20
+ The current Nuxt Email candidate is compared against **React Email `6.9.0`** and **`@react-email/render` `2.1.0`**. Each behavior is a case that renders the same input through both renderers and asserts one of these classifications:
21
+
22
+ - **exact** — byte-identical output.
23
+ - **normalized** — identical after the internal conformance helper canonicalizes insignificant serialization differences such as attribute order, trailing `style` semicolons, class whitespace, and self-closing form.
24
+ - **semantic** — specific asserted properties match.
25
+ - **intentional-divergence** — output differs on purpose, for a documented email-safety or Vue-authoring reason.
26
+ - **unsupported** — a React Email component that is not ported.
27
+
28
+ ## Current results
29
+
30
+ The generated repository report is the only source for current counts, classifications, fixed commits, and hashes. This page does not copy values that can drift. See the [generated conformance report](https://github.com/lupinum-dev/nuxt-email/blob/main/docs/conformance/report.md) and its [provenance record](https://github.com/lupinum-dev/nuxt-email/blob/main/docs/conformance/provenance.md).
31
+
32
+ ## Intentional divergences
33
+
34
+ | Case | Reason |
35
+ | --- | --- |
36
+ | `html-defaults` | Vue SSR does not inject React 19's implicit empty head into a standalone `html` element. |
37
+ | `preview-max-length` | `EPreview` omits React 19 title output and keeps its hiding and plain-text exclusion invariant, because Vue SSR cannot safely hoist title into head. |
38
+ | `preview-short` | React 19 hoists Preview title into head; Vue authors place title explicitly in `EHead`. `EPreview` keeps hiding styles and `data-skip-in-text` fixed so user attributes cannot expose filler. |
39
+ | `preview-style-override` | React replaces all hiding styles when a user style is provided; `EPreview` retains the hiding defaults as an email-safety invariant. |
40
+ | `preview-unicode-boundary` | React truncates at 200 UTF-16 code units and can split a surrogate pair; `EPreview` drops the whole boundary code point and fills the remaining position. |
41
+ | `row-columns` | `EColumn` omits React Email's internal `data-id` marker because no Vue behavior consumes it. |
42
+
43
+ ## Additional documented differences
44
+
45
+ - **`EMarkdown` drops `data-id`.** React Email wraps Markdown in `<div data-id="react-email-markdown">`; `EMarkdown` omits the marker (the same no-data-id divergence as `EColumn`).
46
+ - **Presentation tables reject fixed attributes.** `ESection`, `EContainer`, and `ERow` throw a `TypeError` when passed `border`, `cellpadding`, `cellspacing`, or `role`. React Email silently discards these overrides; Nuxt Email fails loudly to keep the email-safe table layout an invariant.
47
+ - **`ECodeInline` excludes its compatibility copy from plain text.** HTML retains the hidden `Orange.fr` fallback span, but plain-text conversion skips it so recipients receive the code once. React Email emits it twice.
48
+ - **`ETailwind` moves non-inlinable rules to `<head>`.** Media-query and pseudo-class rules move to a `<style>` in `<head>`. This needs a head inside `ETailwind`; rendering fails without one. Residual class names are sanitized. Outlook-specific (`mso-*`) properties survive inlining.
49
+ - **`ETailwind` reaches classes inside nested components.** Style-bearing primitives resolve their classes during rendering. A second pass handles marked plain and structural elements without changing other output. Nested `ETailwind` boundaries are not supported. See the [Tailwind guide](/raw/docs/guides/tailwind.md).
50
+
51
+ ## Scope, not a parity guarantee
52
+
53
+ The report records zero failing runnable cases and lists unsupported React components. Its runnable scope is the eighteen built-ins. It records opt-in `ECodeBlock` as an intentional difference from React Email's CodeBlock.
54
+
55
+ This does not mean every React Email component or option has a Nuxt Email equivalent. Use the generated repository report as the authoritative matrix of cases, classifications, differences, and unsupported surface.
@@ -0,0 +1,69 @@
1
+ ---
2
+ title: "defineEmail"
3
+ description: "Declare a template subject or authored plain-text alternative from script setup."
4
+ url: "https://nuxt-email.lupinum.com/docs/reference/define-email"
5
+ route: "/docs/reference/define-email"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # defineEmail
13
+
14
+ > Declare a template subject or authored plain-text alternative from script setup.
15
+
16
+ `defineEmail(options)` declares metadata for the current email render. Import it only in an email template.
17
+
18
+ ```vue [app/emails/welcome.vue]
19
+ <script setup lang="ts">
20
+ import { defineEmail } from '@lupinum/nuxt-email/define-email'
21
+
22
+ const props = defineProps<{
23
+ firstName: string
24
+ }>()
25
+
26
+ defineEmail({
27
+ subject: () => `Welcome aboard, ${props.firstName}`,
28
+ text: () => `Welcome aboard, ${props.firstName}. Your account is ready.`,
29
+ })
30
+ </script>
31
+ ```
32
+
33
+ ## Signature
34
+
35
+ ```ts
36
+ type EmailMetadataValue = string | (() => string)
37
+
38
+ type DefineEmailOptions
39
+ = | { subject: EmailMetadataValue, text?: EmailMetadataValue }
40
+ | { subject?: EmailMetadataValue, text: EmailMetadataValue }
41
+
42
+ function defineEmail(options: DefineEmailOptions): void
43
+ ```
44
+
45
+ At least one of `subject` or `text` is required. Extra keys and other value types fail. Functions take no arguments, run synchronously after HTML rendering, and can close over the template's real `defineProps()` value. An asynchronous function is not supported.
46
+
47
+ ## Metadata precedence
48
+
49
+ | Metadata | Result |
50
+ | --- | --- |
51
+ | `subject` declared | `RenderedEmail.subject` contains the resolved string. |
52
+ | `subject` absent | `RenderedEmail.subject` is absent. No subject is derived. |
53
+ | `text` declared | `RenderedEmail.text` contains the authored string. |
54
+ | `text` absent | `RenderedEmail.text` is derived from the final HTML. |
55
+
56
+ Call `defineEmail()` once per template render. It may run before or after a top-level `await` in `<script setup>`. Calling it outside an email render throws `DefineEmailOutsideRenderError`; calling it twice in one render throws `DuplicateEmailDefinitionError`.
57
+
58
+ ## Entry-point exports
59
+
60
+ Import from `@lupinum/nuxt-email/define-email`:
61
+
62
+ | Export | Kind |
63
+ | --- | --- |
64
+ | `defineEmail` | Runtime function |
65
+ | `DefineEmailOutsideRenderError` | Runtime error class |
66
+ | `DuplicateEmailDefinitionError` | Runtime error class |
67
+ | `DefineEmailOptions` | Type only |
68
+
69
+ See [subject and authored text](/raw/docs/guides/subject-lines.md) for task-oriented examples and [error types](/raw/docs/reference/errors.md) for recovery guidance.
@@ -0,0 +1,72 @@
1
+ ---
2
+ title: "Error types"
3
+ description: "Public error classes, their stable fields, common symptoms, and recovery steps."
4
+ url: "https://nuxt-email.lupinum.com/docs/reference/errors"
5
+ route: "/docs/reference/errors"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Error types
13
+
14
+ > Public error classes, their stable fields, common symptoms, and recovery steps.
15
+
16
+ Nuxt Email exposes five runtime error classes from `@lupinum/nuxt-email/errors`. Original render failures remain available through `cause`.
17
+
18
+ ## Public errors
19
+
20
+ | Error | When it occurs | Stable fields | What to do |
21
+ | --- | --- | --- | --- |
22
+ | `EmailRenderError` | Loading, prop validation, Vue server rendering, document validation, or plain-text conversion fails. | `templateName`; original failure on `cause` | Log the error and inspect `cause`. Fix the template, props, or document named by `templateName`. |
23
+ | `UnknownEmailTemplateError` | An untyped runtime name is absent from the generated registry. | `requestedName`; sorted `knownNames` | Use one of `knownNames`, or add the template and run `nuxt prepare`. |
24
+ | `TailwindMissingHeadError` | A Tailwind media-query or pseudo-class rule has no `<head>` inside its `ETailwind` boundary. | Message names the affected classes. | Move `EHead` inside `ETailwind`, or remove the classes that require head CSS. |
25
+ | `DefineEmailOutsideRenderError` | `defineEmail()` runs outside an email render. | Error identity and message | Call it from an email template rendered by Nuxt Email. |
26
+ | `DuplicateEmailDefinitionError` | One render calls `defineEmail()` more than once. | Error identity and message | Keep one call and combine its `subject` and `text` metadata. |
27
+
28
+ `EmailRenderError.templateName` is the registry name for `renderEmail()`. Standalone rendering uses the Vue component name. There is no `componentName` compatibility alias.
29
+
30
+ ## Find an error by symptom
31
+
32
+ | Search phrase or symptom | Error | Next check |
33
+ | --- | --- | --- |
34
+ | Template not found, unknown template, or unknown email | `UnknownEmailTemplateError` | Compare `requestedName` with `knownNames`. Nested names use `/` and omit `.vue`. |
35
+ | Missing head, `<head> not found`, or Tailwind variant failure | `TailwindMissingHeadError` | Confirm that `EHead` is inside the same `ETailwind` boundary. |
36
+ | `defineEmail` outside render | `DefineEmailOutsideRenderError` | Keep the call in the template's `<script setup>`. |
37
+ | Duplicate email definition | `DuplicateEmailDefinitionError` | Search the template and its setup path for a second call. |
38
+ | Render failed, invalid document, or invalid props | `EmailRenderError` | Inspect `error.cause`; it preserves the underlying failure. |
39
+
40
+ There is no `TemplateNotFoundError`. The public class for that condition is `UnknownEmailTemplateError`.
41
+
42
+ ## Imports
43
+
44
+ ```ts
45
+ import {
46
+ DefineEmailOutsideRenderError,
47
+ DuplicateEmailDefinitionError,
48
+ EmailRenderError,
49
+ TailwindMissingHeadError,
50
+ UnknownEmailTemplateError,
51
+ } from '@lupinum/nuxt-email/errors'
52
+ ```
53
+
54
+ `EmailRenderError` is also exported by `@lupinum/nuxt-email/testing`. The two metadata errors are also exported by `@lupinum/nuxt-email/define-email`.
55
+
56
+ Template discovery can report `DuplicateEmailTemplateError` or `EmailTemplateDiscoveryError` during module setup. These are diagnostics, not exports from the public error entry point.
57
+
58
+ ## Production handling
59
+
60
+ Log the full error on the server, including `cause`. Return a generic response to the caller. Nuxt Email does not serialize filesystem paths or stacks into production responses. The [development preview](/raw/docs/guides/preview-workflow.md) shows richer local details for authoring.
61
+
62
+ ```ts [server/api/welcome.get.ts]
63
+ export default defineEventHandler(async () => {
64
+ try {
65
+ return await renderEmail('welcome', props)
66
+ }
67
+ catch (error) {
68
+ console.error(error)
69
+ throw createError({ statusCode: 500, statusMessage: 'Email render failed' })
70
+ }
71
+ })
72
+ ```
@@ -0,0 +1,144 @@
1
+ ---
2
+ title: "Migration from React Email"
3
+ description: "Translate JSX templates, props, and rendering calls to Nuxt Email's Vue single-file components and generated server API."
4
+ url: "https://nuxt-email.lupinum.com/docs/reference/migration"
5
+ route: "/docs/reference/migration"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Migration from React Email
13
+
14
+ > Translate JSX templates, props, and rendering calls to Nuxt Email's Vue single-file components and generated server API.
15
+
16
+ ## Upgrade from 0.1.1
17
+
18
+ `1.0.0-beta.1` makes four intentional hard cutovers:
19
+
20
+ - Add a non-empty `lang` to every `EHtml`.
21
+ - Style `ELink` color and text decoration explicitly when the design needs them; the component is now visually neutral.
22
+ - Replace reads of `EmailRenderError.componentName` with `templateName`. No deprecated alias is provided.
23
+ - Give `defineEmail` at least one synchronous `subject` or `text` value. Authored `text` now replaces derived plain text.
24
+
25
+ For preview fixtures, replace the longer Vue utility-type expression with `EmailComponentProps<typeof YourEmail>`. Fixtures remain deterministic objects, not factories or scenarios.
26
+
27
+ Nuxt Email ports proven email-output behavior, not React's runtime or component API. Move a template by preserving its semantic document, replacing React components with the E-prefixed Vue primitives, and exposing it through Nuxt's generated server registry.
28
+
29
+ ## The same email, two frameworks
30
+
31
+ ### React Email
32
+
33
+ ```tsx
34
+ import { Body, Button, Container, Head, Heading, Html, Preview, Text } from '@react-email/components'
35
+
36
+ export function OrderEmail({ orderNumber, recipientName }: { orderNumber: number; recipientName: string }) {
37
+ return (
38
+ <Html lang="en">
39
+ <Head><title>Order confirmation</title></Head>
40
+ <Preview>Your order is ready.</Preview>
41
+ <Body style={{ backgroundColor: '#f5f5f5', margin: 0 }}>
42
+ <Container style={{ maxWidth: '600px', padding: '24px' }}>
43
+ <Heading>Order {orderNumber} for {recipientName}</Heading>
44
+ <Text>We have received your order.</Text>
45
+ <Button href={`https://example.com/orders/${orderNumber}`} style={{ padding: '12px 20px' }}>
46
+ View order
47
+ </Button>
48
+ </Container>
49
+ </Body>
50
+ </Html>
51
+ )
52
+ }
53
+ ```
54
+
55
+ ### Nuxt Email
56
+
57
+ Place the Vue version at `app/emails/order-confirmation.vue`:
58
+
59
+ ```vue
60
+ <script setup lang="ts">
61
+ import { defineEmail } from '@lupinum/nuxt-email/define-email'
62
+
63
+ const props = defineProps<{
64
+ orderNumber: number
65
+ recipientName: string
66
+ }>()
67
+
68
+ defineEmail({
69
+ subject: () => `Order ${props.orderNumber} confirmed`,
70
+ })
71
+ </script>
72
+
73
+ <template>
74
+ <EHtml lang="en">
75
+ <EHead><title>Order confirmation</title></EHead>
76
+ <EBody :style="{ backgroundColor: '#f5f5f5', margin: 0 }">
77
+ <EPreview>Your order is ready.</EPreview>
78
+ <EContainer :style="{ maxWidth: '600px', padding: '24px' }">
79
+ <EHeading>Order {{ orderNumber }} for {{ recipientName }}</EHeading>
80
+ <EText>We have received your order.</EText>
81
+ <EButton :href="`https://example.com/orders/${orderNumber}`" :style="{ padding: '12px 20px' }">
82
+ View order
83
+ </EButton>
84
+ </EContainer>
85
+ </EBody>
86
+ </EHtml>
87
+ </template>
88
+ ```
89
+
90
+ `EPreview` belongs inside `EBody`, and the document title goes explicitly in `EHead` — Vue SSR does not reproduce React 19's preview-title hoisting (an [intentional divergence](/raw/docs/reference/conformance.md)).
91
+
92
+ ## Rendering
93
+
94
+ React Email renders an element directly and requests HTML and plain text separately:
95
+
96
+ ```tsx
97
+ const html = await render(<OrderEmail {...props} />)
98
+ const text = await render(<OrderEmail {...props} />, { plainText: true })
99
+ ```
100
+
101
+ Nuxt Email discovers the Vue single-file component (`.vue` file) as `order-confirmation`. It generates its prop type and returns both representations from one server renderer:
102
+
103
+ ```ts [server/api/order-confirmation.get.ts]
104
+ export default defineEventHandler(() => {
105
+ return renderEmail('order-confirmation', {
106
+ orderNumber: 7319,
107
+ recipientName: 'Ada',
108
+ })
109
+ })
110
+ ```
111
+
112
+ Do not import a component renderer into application code. The public path is the generated Nitro-only [`renderEmail`](/raw/docs/reference/render-email.md) auto-import.
113
+
114
+ ## Authoring translation
115
+
116
+ | React Email | Nuxt Email |
117
+ | --- | --- |
118
+ | JSX/TSX component | Vue `.vue` file under `app/emails/` |
119
+ | Imported `Html`, `Body`, `Text`, `Button` | Auto-registered `EHtml`, `EBody`, `EText`, `EButton` |
120
+ | TypeScript function props | Standard Vue `defineProps<>()` |
121
+ | `children` | Default Vue slot |
122
+ | `{condition && <Text />}` | `<EText v-if="condition">` |
123
+ | `{items.map(item => <Text key={item.id} />)}` | `<EText v-for="item in items" :key="item.id">` |
124
+ | `style={{ backgroundColor: '#fff' }}` | `:style="{ backgroundColor: '#fff' }"` |
125
+ | `<Tailwind>` wrapper | Auto-registered [`ETailwind`](/raw/docs/components/tailwind.md) |
126
+ | Renderer receives an element | Renderer receives a generated template name and typed props |
127
+ | Preview-specific React app | Exact sibling fixture plus development-only `/__email` |
128
+
129
+ Use Vue's native attribute names and CSS styles. React-shaped convenience props such as `mx` are intentionally absent — use ordinary CSS in `style`.
130
+
131
+ `ETailwind` uses Tailwind v4, but it does not automatically inherit your Nuxt stylesheet or browser CSS variables. Pass concrete `theme`/`config` values from your application-owned token source when migrating custom utilities such as `text-primary`.
132
+
133
+ ## Behavioral differences to review
134
+
135
+ - Compatibility is tracked **per behavior**, never as a global React Email compatibility claim.
136
+ - Component names and the authoring API are Vue-native and E-prefixed.
137
+ - React streaming, Suspense behavior, generated React markers, JSX execution, and the React preview stack are not ported.
138
+ - Some document, preview, and table-marker output intentionally differs where Vue authoring or email safety requires it.
139
+
140
+ Do not use this summary as a parity matrix — review the [conformance report](/raw/docs/reference/conformance.md) for the authoritative cases and divergences.
141
+
142
+ ## Sending remains application-owned
143
+
144
+ Nuxt Email stops at `{ html, text, subject? }`. Keep recipients, sender identity, credentials, attachments, tags, scheduling, and delivery policy in your provider SDK. Nuxt Email has no provider-neutral adapter or public send endpoint. You can keep provider-specific features and optionally compute the subject beside the template with [`defineEmail`](/raw/docs/guides/subject-lines.md).
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: "Module behavior"
3
+ description: "What the module registers, its fixed conventions, and the opt-in codeBlock configuration."
4
+ url: "https://nuxt-email.lupinum.com/docs/reference/module"
5
+ route: "/docs/reference/module"
6
+ locale: "en"
7
+ section: "Documentation"
8
+ collection: "docs"
9
+ source: "ginko-content"
10
+ ---
11
+
12
+ # Module behavior
13
+
14
+ > What the module registers, its fixed conventions, and the opt-in codeBlock configuration.
15
+
16
+ Nuxt Email is a standard Nuxt module registered in `modules`. Discovery, preview routes, and the render API are fixed by convention. Syntax-highlighted blocks are the one opt-in feature.
17
+
18
+ ```ts [nuxt.config.ts]
19
+ export default defineNuxtConfig({
20
+ modules: ['@lupinum/nuxt-email'],
21
+ })
22
+ ```
23
+
24
+ ## What the module registers
25
+
26
+ - **Components.** Eighteen built-in `E*` components are auto-imported for use inside email templates. Configuring `codeBlock` adds `ECodeBlock`.
27
+ - **`renderEmail`** — a Nitro-only auto-import generated from the discovered templates, typed with each template's name and props.
28
+ - **`defineEmail` entry point** — import it from `@lupinum/nuxt-email/define-email` inside a template to declare [subject or authored text metadata](/raw/docs/reference/define-email.md).
29
+ - **Development preview** — the `/__email` application and its endpoints, registered only when `nuxt.options.dev` is true.
30
+ - **Nuxt DevTools shortcut** — a development-only iframe tab pointing to that same baseURL-aware preview.
31
+
32
+ ## Package entry points
33
+
34
+ The published package exposes six entry points. Runtime and type-only exports are listed separately.
35
+
36
+ | Import | Runtime exports | Type-only exports |
37
+ | --- | --- | --- |
38
+ | `@lupinum/nuxt-email` | Default Nuxt module | `ModuleOptions`, `RenderedEmail`, `EmailComponentProps` |
39
+ | `@lupinum/nuxt-email/build` | `buildEmailRegistry` (Node build only) | `BuildEmailRegistryOptions` |
40
+ | `@lupinum/nuxt-email/render` | `renderEmailComponent`, `EmailRenderError`, `UnknownEmailTemplateError` | `RenderedEmail`, `EmailComponentProps`, `EmailComponents` |
41
+ | `@lupinum/nuxt-email/define-email` | `defineEmail`, `DefineEmailOutsideRenderError`, `DuplicateEmailDefinitionError` | `DefineEmailOptions` |
42
+ | `@lupinum/nuxt-email/testing` | `renderEmailComponent`, `EmailRenderError` | `RenderedEmail` |
43
+ | `@lupinum/nuxt-email/errors` | `EmailRenderError`, `UnknownEmailTemplateError`, `DefineEmailOutsideRenderError`, `DuplicateEmailDefinitionError`, `TailwindMissingHeadError` | — |
44
+
45
+ After `nuxt prepare`, the generated `#nuxt-email/testing` alias supplies the configured testing renderer. Use it when a template depends on generated component configuration such as `ECodeBlock`. It follows the same `renderEmailComponent` and `EmailRenderError` testing surface.
46
+
47
+ There is no public `renderEmail` package export; its type is generated from each application's templates.
48
+ For a server registry without Nitro, see [Render outside Nitro](/raw/docs/guides/standalone-rendering.md).
49
+
50
+ ## Template discovery
51
+
52
+ - Templates are `.vue` files under `app/emails/`.
53
+ - Nested paths become slash-separated names (`app/emails/account/reset-password.vue` → `account/reset-password`).
54
+ - `app/emails/components/` is reserved for your own supporting components and is not discovered.
55
+ - Discovery follows the active Nuxt `srcDir`: a custom `srcDir: 'src/'` uses `src/emails/`.
56
+ - Email templates from inherited Nuxt layers are not merged into the active application's registry. Keep the application-owned templates in its active `srcDir/emails` tree.
57
+ - In a monorepo, register the module in each Nuxt application that sends email. Each application owns one registry from its own active `srcDir/emails`; package and layer templates are not merged implicitly.
58
+ - Sibling `.fixtures.ts` files supply deterministic preview props in development and are excluded from production output.
59
+
60
+ The registry is regenerated when templates are added, renamed, or deleted, which changes the generated server types. Run `nuxt prepare` after such changes.
61
+
62
+ ## Optional syntax highlighting
63
+
64
+ `codeBlock` is the only module option. It uses the Shiki syntax highlighter. The option requires one theme and a non-empty, closed language allowlist:
65
+
66
+ | Option | Type | Default | Required |
67
+ | --- | --- | --- | --- |
68
+ | `codeBlock` | `{ languages: readonly string[], theme: string }` | — | No |
69
+ | `codeBlock.languages` | Non-empty `readonly string[]` | — | Yes when `codeBlock` is set |
70
+ | `codeBlock.theme` | `string` | — | Yes when `codeBlock` is set |
71
+
72
+ ```ts
73
+ export default defineNuxtConfig({
74
+ modules: ['@lupinum/nuxt-email'],
75
+ nuxtEmail: {
76
+ codeBlock: {
77
+ languages: ['typescript', 'vue'],
78
+ theme: 'github-dark',
79
+ },
80
+ },
81
+ })
82
+ ```
83
+
84
+ Only the configured languages and the grammars they import enter the server bundle. Nuxt Email creates the highlighter on first use. Without this option, `ECodeBlock` is unregistered and Shiki performs no module-setup or production-bundle work. Shiki remains an installation-time dependency. Unsupported top-level options fail during setup.
85
+
86
+ ## Production boundary
87
+
88
+ The production server keeps templates referenced by its registry because Nitro renders them there. The client bundle excludes those templates.
89
+
90
+ The `/__email` application, its endpoints, and every discovered `.fixtures.ts` module are absent from production output. See the [preview workflow](/raw/docs/guides/preview-workflow.md) and [security boundary](/raw/docs/reference/render-email.md#security-boundary).