@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.
- package/CHANGELOG.md +35 -1
- package/README.md +31 -3
- package/dist/agent/AGENTS.md +47 -0
- package/dist/agent/manifest.json +208 -0
- package/dist/agent/pages/docs/components/code.md +68 -0
- package/dist/agent/pages/docs/components/content.md +111 -0
- package/dist/agent/pages/docs/components/layout.md +89 -0
- package/dist/agent/pages/docs/components/markdown.md +49 -0
- package/dist/agent/pages/docs/components/structure.md +122 -0
- package/dist/agent/pages/docs/components/tailwind.md +41 -0
- package/dist/agent/pages/docs/components.md +56 -0
- package/dist/agent/pages/docs/getting-started/choose-a-tool.md +38 -0
- package/dist/agent/pages/docs/getting-started/first-email.md +99 -0
- package/dist/agent/pages/docs/getting-started/installation.md +97 -0
- package/dist/agent/pages/docs/getting-started/render-from-nitro.md +59 -0
- package/dist/agent/pages/docs/getting-started.md +62 -0
- package/dist/agent/pages/docs/guides/client-compatibility.md +65 -0
- package/dist/agent/pages/docs/guides/plain-text.md +42 -0
- package/dist/agent/pages/docs/guides/preview-workflow.md +83 -0
- package/dist/agent/pages/docs/guides/sending-email.md +132 -0
- package/dist/agent/pages/docs/guides/standalone-rendering.md +118 -0
- package/dist/agent/pages/docs/guides/subject-lines.md +95 -0
- package/dist/agent/pages/docs/guides/tailwind.md +128 -0
- package/dist/agent/pages/docs/guides/testing-your-emails.md +118 -0
- package/dist/agent/pages/docs/guides.md +32 -0
- package/dist/agent/pages/docs/reference/conformance.md +55 -0
- package/dist/agent/pages/docs/reference/define-email.md +69 -0
- package/dist/agent/pages/docs/reference/errors.md +72 -0
- package/dist/agent/pages/docs/reference/migration.md +144 -0
- package/dist/agent/pages/docs/reference/module.md +90 -0
- package/dist/agent/pages/docs/reference/render-email.md +107 -0
- package/dist/agent/pages/docs/reference.md +30 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +3 -3
- package/package.json +15 -9
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Client compatibility and proofs"
|
|
3
|
+
description: "What is automatically proven, and the manual email-client QA checklist that closes the remaining gap."
|
|
4
|
+
url: "https://nuxt-email.lupinum.com/docs/guides/client-compatibility"
|
|
5
|
+
route: "/docs/guides/client-compatibility"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Client compatibility and proofs
|
|
13
|
+
|
|
14
|
+
> What is automatically proven, and the manual email-client QA checklist that closes the remaining gap.
|
|
15
|
+
|
|
16
|
+
Nuxt Email separates automated compatibility evidence from manual client results.
|
|
17
|
+
|
|
18
|
+
## What is automatically proven
|
|
19
|
+
|
|
20
|
+
The [conformance suite](/raw/docs/reference/conformance.md) compares covered behavior with fixed React Email versions. It checks exact bytes, normalized output, or specific semantics. Presentation tables, Outlook-specific button spacers, preview hiding, font declarations, and Tailwind inlining have regression cases.
|
|
21
|
+
|
|
22
|
+
This is useful implementation evidence, but it is a comparison to a reference renderer, not proof that every email client will display a message identically.
|
|
23
|
+
|
|
24
|
+
## Completed beta smoke test
|
|
25
|
+
|
|
26
|
+
Matthias completed the owner-run `1.0.0-beta.1` smoke test on 2026-08-22. The same `proof-kit` message passed in Gmail web, Apple Mail on macOS, and Outlook classic on Windows in light and dark modes. The [signed QA record](https://github.com/lupinum-dev/nuxt-email/blob/main/docs/release/client-qa-checklist.md#100-beta1-smoke-test) records the tested commit and artifact hashes.
|
|
27
|
+
|
|
28
|
+
This smoke test is enough for the published beta. It does not complete the full eight-client checklist required for stable `1.0.0`. An external transactional beta also remains pending.
|
|
29
|
+
|
|
30
|
+
## Run the stable checklist
|
|
31
|
+
|
|
32
|
+
Automated tests do not open real email clients. Run the stable checklist against the fixed `proof-kit` fixture.
|
|
33
|
+
|
|
34
|
+
<steps>
|
|
35
|
+
### Render the fixture
|
|
36
|
+
|
|
37
|
+
Run `pnpm proofs:generate`, then use the committed `proof-kit` template and props. Optionally replace only the logo URL with an HTTPS image you control.
|
|
38
|
+
|
|
39
|
+
### Send with your own account
|
|
40
|
+
|
|
41
|
+
Send the exact rendered `html` and `text` to test mailboxes through a provider SDK you choose. Nuxt Email does not send mail.
|
|
42
|
+
|
|
43
|
+
### Inspect in three clients
|
|
44
|
+
|
|
45
|
+
Open the same message in **Gmail web**, **Apple Mail** (macOS or iOS), and **Outlook for Windows**.
|
|
46
|
+
|
|
47
|
+
### Record the result
|
|
48
|
+
|
|
49
|
+
Note client versions, screenshots, defects, and a final accept/reject decision.
|
|
50
|
+
</steps>
|
|
51
|
+
|
|
52
|
+
## Required checks
|
|
53
|
+
|
|
54
|
+
- Inbox preview shows only the intended preview sentence and does not expose filler characters.
|
|
55
|
+
- The remote logo has useful alternative text when images are blocked.
|
|
56
|
+
- Heading, paragraphs, divider, and footer links retain readable spacing.
|
|
57
|
+
- The activation button is clickable, visually centered, and has comparable horizontal and vertical padding **in Outlook**.
|
|
58
|
+
- Two columns stay in one row at desktop email widths and remain readable when the client narrows the message.
|
|
59
|
+
- Link destinations and the `mailto:` link are correct.
|
|
60
|
+
- Plain-text fallback contains the meaningful content and destinations but excludes preview filler and images.
|
|
61
|
+
- No script, hydration payload, framework state, or event-handler attribute is present.
|
|
62
|
+
|
|
63
|
+
<card title="Beta QA is not stable approval" icon="lucide:shield-alert">
|
|
64
|
+
The three-client smoke test is complete. Stable `1.0.0` still waits for the full eight-client record and external beta evidence.
|
|
65
|
+
</card>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Plain text"
|
|
3
|
+
description: "How the plain-text fallback is produced from the rendered HTML, what it excludes, and how to control table output."
|
|
4
|
+
url: "https://nuxt-email.lupinum.com/docs/guides/plain-text"
|
|
5
|
+
route: "/docs/guides/plain-text"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Plain text
|
|
13
|
+
|
|
14
|
+
> How the plain-text fallback is produced from the rendered HTML, what it excludes, and how to control table output.
|
|
15
|
+
|
|
16
|
+
Every render returns a `text` string alongside `html`. By default it is a deterministic fallback produced from the **final rendered HTML**. Nuxt Email uses a fixed `html-to-text` version with no line wrapping.
|
|
17
|
+
|
|
18
|
+
When automatic conversion is not good enough, the template can declare authored text with `defineEmail({ text: '…' })` or a synchronous `text` function. Authored text replaces derivation for that render; it does not create a second template or rendering path.
|
|
19
|
+
|
|
20
|
+
## What plain text includes and excludes
|
|
21
|
+
|
|
22
|
+
- **Excluded:** head content, images, `<script>`/`<style>` content, and any element marked `data-skip-in-text="true"`. `EPreview` is therefore absent from the fallback.
|
|
23
|
+
- **Links:** destinations are retained when the visible text differs from the URL. Fragment links and `mailto:` links follow the recorded React Email comparison behavior.
|
|
24
|
+
- **Structure:** nested lists, ordered-list starts, blockquotes, hard breaks, preformatted content, Unicode, and ordinary tables all have exact conformance cases.
|
|
25
|
+
|
|
26
|
+
## Controlling table output
|
|
27
|
+
|
|
28
|
+
By default tables render as plain rows. When you specifically need aligned, column-formatted output for a table, add `data-text-format="dataTable"` to that table:
|
|
29
|
+
|
|
30
|
+
```vue
|
|
31
|
+
<table data-text-format="dataTable">
|
|
32
|
+
<!-- rows -->
|
|
33
|
+
</table>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Inspect it every time
|
|
37
|
+
|
|
38
|
+
Plain text is a real deliverable that some recipients and spam filters read. Inspect it in the [development preview](/raw/docs/guides/preview-workflow.md)'s **Plain text** tab for every transactional template, exactly as you would review the HTML.
|
|
39
|
+
|
|
40
|
+
<card title="ECodeInline appears once" icon="lucide:info">
|
|
41
|
+
`ECodeInline` keeps two copies in HTML for `Orange.fr` compatibility, but marks the hidden copy to be skipped by plain-text generation. Recipients therefore receive the code once in the text alternative. This is an intentional divergence from React Email.
|
|
42
|
+
</card>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Preview workflow"
|
|
3
|
+
description: "Use the development-only /__email preview, add fixtures, and monitor exact rendered size without treating it as a client guarantee."
|
|
4
|
+
url: "https://nuxt-email.lupinum.com/docs/guides/preview-workflow"
|
|
5
|
+
route: "/docs/guides/preview-workflow"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Preview workflow
|
|
13
|
+
|
|
14
|
+
> Use the development-only /__email preview, add fixtures, and monitor exact rendered size without treating it as a client guarantee.
|
|
15
|
+
|
|
16
|
+
Nuxt Email exposes `/__email` while the development server is running. The path follows the configured Nuxt base URL. Open `/__email` with the default base URL, or `/sub/__email` with `app.baseURL: '/sub/'`.
|
|
17
|
+
|
|
18
|
+
The preview lists every template under `app/emails/` and marks templates without fixture data. It shows rendered HTML, exact HTML source, or plain text. The visible page polls once per second, so saved template and fixture changes appear without a restart. Polling pauses in a hidden tab.
|
|
19
|
+
|
|
20
|
+
When Nuxt DevTools is available, its **Nuxt Email** iframe shortcut opens the same baseURL-aware preview. The shortcut adds no second preview application or route.
|
|
21
|
+
|
|
22
|
+
The preview calls the same server-only registry and `renderEmail()` implementation your handlers use. It is a rendering and debugging tool — not a second rendering path and not an email-client compatibility guarantee.
|
|
23
|
+
|
|
24
|
+
## Add a fixture
|
|
25
|
+
|
|
26
|
+
Place one exact sibling file beside a template. Only the `.fixtures.ts` suffix is recognized, and it must default-export one props object:
|
|
27
|
+
|
|
28
|
+
```ts [app/emails/welcome.fixtures.ts]
|
|
29
|
+
import type { EmailComponentProps } from '@lupinum/nuxt-email'
|
|
30
|
+
import type WelcomeEmail from './welcome.vue'
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
firstName: 'Ada',
|
|
34
|
+
activationUrl: 'https://example.com/activate',
|
|
35
|
+
} satisfies EmailComponentProps<typeof WelcomeEmail>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The preview intentionally supports **one fixed scenario per template** and does not accept request-provided props or generate editing forms.
|
|
39
|
+
|
|
40
|
+
## Inspect output
|
|
41
|
+
|
|
42
|
+
<steps>
|
|
43
|
+
### Preview
|
|
44
|
+
|
|
45
|
+
Loads the raw rendered HTML in a sandboxed iframe (no script permission).
|
|
46
|
+
|
|
47
|
+
### HTML
|
|
48
|
+
|
|
49
|
+
Displays the exact `html` returned by `renderEmail()`.
|
|
50
|
+
|
|
51
|
+
### Plain text
|
|
52
|
+
|
|
53
|
+
Displays the exact `text` returned by `renderEmail()`.
|
|
54
|
+
|
|
55
|
+
### Copy / Open
|
|
56
|
+
|
|
57
|
+
**Copy** copies the active representation; **Open** opens the raw development render in a separate tab.
|
|
58
|
+
</steps>
|
|
59
|
+
|
|
60
|
+
Render failures show the template name, the `EmailRenderError` stack, and its original cause. Use the [error reference](/raw/docs/reference/errors.md) to find the recovery step by symptom.
|
|
61
|
+
|
|
62
|
+
A template without a fixture remains in the list. Add its sibling fixture before trying to render it.
|
|
63
|
+
|
|
64
|
+
## Mind the approximate Gmail clipping budget
|
|
65
|
+
|
|
66
|
+
Gmail is commonly observed clipping messages around **102 KB**, but behavior can vary with the account, message, and delivery path. Nuxt Email reports the rendered HTML's exact UTF-8 byte count; the Gmail-specific interpretation is an approximate authoring warning, not a guarantee:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const { html } = await renderEmail('welcome', props)
|
|
70
|
+
console.log('bytes:', Buffer.byteLength(html, 'utf8'))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Keep the rendered HTML comfortably below the warning area and test the delivered message in your target Gmail accounts. Inlined Tailwind utilities and repeated markup add up quickly, so measure your largest, most dynamic template.
|
|
74
|
+
|
|
75
|
+
The preview has no dark-mode simulation. Email clients transform dark-mode content differently, so test dark rendering in the real clients you support.
|
|
76
|
+
|
|
77
|
+
## Production boundary
|
|
78
|
+
|
|
79
|
+
The preview page, endpoints, and fixture imports exist **only in development**. Production builds contain none of their routes or fixture data. The raw render endpoint uses a restrictive content security policy. Its iframe has no script permission.
|
|
80
|
+
|
|
81
|
+
The development endpoints cannot become production render or send APIs. Use the typed server-only [`renderEmail`](/raw/docs/reference/render-email.md) in your handlers.
|
|
82
|
+
|
|
83
|
+
Next, [render the template from Nitro](/raw/docs/getting-started/render-from-nitro.md).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Sending email"
|
|
3
|
+
description: "Hand Nuxt Email output to a provider SDK or a local Mailpit SMTP server."
|
|
4
|
+
url: "https://nuxt-email.lupinum.com/docs/guides/sending-email"
|
|
5
|
+
route: "/docs/guides/sending-email"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Sending email
|
|
13
|
+
|
|
14
|
+
> Hand Nuxt Email output to a provider SDK or a local Mailpit SMTP server.
|
|
15
|
+
|
|
16
|
+
Nuxt Email renders content; your application sends it. This keeps recipients, credentials, retries, idempotency, rate limits, and delivery logs in application code where they belong.
|
|
17
|
+
|
|
18
|
+
`renderEmail` returns `html`, `text`, and an optional `subject`. Pass those values directly to your delivery client. Decide the fallback subject in the application when a template does not declare one.
|
|
19
|
+
|
|
20
|
+
## Provider SDK example
|
|
21
|
+
|
|
22
|
+
Install your provider's SDK in the application. This example uses [Resend's Node SDK](https://resend.com/docs/api-reference/emails/send-email), whose send call accepts `subject`, `html`, and `text`:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm add resend
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ts [server/utils/send-welcome.ts]
|
|
29
|
+
import { Resend } from 'resend'
|
|
30
|
+
|
|
31
|
+
const resend = new Resend(process.env.NUXT_EMAIL_RESEND_API_KEY)
|
|
32
|
+
|
|
33
|
+
export async function sendWelcomeEmail(to: string, firstName: string) {
|
|
34
|
+
const email = await renderEmail('welcome', {
|
|
35
|
+
firstName,
|
|
36
|
+
activationUrl: 'https://example.com/activate',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const { data, error } = await resend.emails.send({
|
|
40
|
+
from: 'Example <hello@example.com>',
|
|
41
|
+
to,
|
|
42
|
+
subject: email.subject ?? 'Welcome',
|
|
43
|
+
html: email.html,
|
|
44
|
+
text: email.text,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (error) {
|
|
48
|
+
throw error
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return data
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Keep the API key server-only. Validate and authorize the action that chooses `to`; do not accept arbitrary recipients from an unauthenticated request. For production retries, use the provider's idempotency feature so one application event cannot send the same message twice.
|
|
56
|
+
|
|
57
|
+
## Local SMTP with Mailpit
|
|
58
|
+
|
|
59
|
+
[Mailpit](https://mailpit.axllent.org/docs/install/) captures development email instead of delivering it. Its defaults are SMTP on `1025` without authentication or encryption and a browser UI at `http://localhost:8025`. Install Nodemailer in the application:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pnpm add nodemailer
|
|
63
|
+
pnpm add -D @types/nodemailer
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```ts [server/utils/send-welcome-local.ts]
|
|
67
|
+
import nodemailer from 'nodemailer'
|
|
68
|
+
|
|
69
|
+
const mailpit = nodemailer.createTransport({
|
|
70
|
+
host: '127.0.0.1',
|
|
71
|
+
port: 1025,
|
|
72
|
+
secure: false,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export async function sendWelcomeEmailLocally(to: string, firstName: string) {
|
|
76
|
+
const email = await renderEmail('welcome', {
|
|
77
|
+
firstName,
|
|
78
|
+
activationUrl: 'http://localhost:3000/activate',
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return await mailpit.sendMail({
|
|
82
|
+
from: 'Example <hello@example.test>',
|
|
83
|
+
to,
|
|
84
|
+
subject: email.subject ?? 'Welcome',
|
|
85
|
+
html: email.html,
|
|
86
|
+
text: email.text,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
1. Start Mailpit separately.
|
|
92
|
+
2. Run the application action that sends the email.
|
|
93
|
+
3. Open `http://localhost:8025` and inspect the captured message.
|
|
94
|
+
|
|
95
|
+
This tests the provider handoff and the full message with headers. It does not replace the [real email-client checklist](/raw/docs/guides/client-compatibility.md).
|
|
96
|
+
|
|
97
|
+
## Inline images with CID attachments
|
|
98
|
+
|
|
99
|
+
Hosted HTTPS images are usually simpler. Use a Content-ID (CID) attachment when the image bytes must travel inside the message.
|
|
100
|
+
|
|
101
|
+
Reference one unique CID from the template:
|
|
102
|
+
|
|
103
|
+
```vue [app/emails/receipt.vue]
|
|
104
|
+
<EImg src="cid:logo@example.com" alt="Example" width="120" height="32" />
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Give the Nodemailer attachment the exact same `cid` value:
|
|
108
|
+
|
|
109
|
+
```ts [server/utils/send-receipt.ts]
|
|
110
|
+
const email = await renderEmail('receipt', receiptProps)
|
|
111
|
+
|
|
112
|
+
await mailer.sendMail({
|
|
113
|
+
from: 'Example <receipts@example.com>',
|
|
114
|
+
to,
|
|
115
|
+
subject: email.subject ?? 'Your receipt',
|
|
116
|
+
html: email.html,
|
|
117
|
+
text: email.text,
|
|
118
|
+
attachments: [
|
|
119
|
+
{
|
|
120
|
+
filename: 'logo.png',
|
|
121
|
+
path: '/application-owned-assets/logo.png',
|
|
122
|
+
cid: 'logo@example.com',
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
})
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The delivery client reads the attachment bytes and creates the related Multipurpose Internet Mail Extensions (MIME) parts. Nuxt Email only preserves the `cid:` image source. See Nodemailer's [embedded image documentation](https://nodemailer.com/message/embedded-images/).
|
|
129
|
+
|
|
130
|
+
## Ownership boundary
|
|
131
|
+
|
|
132
|
+
Nuxt Email deliberately does not provide a send function, provider adapter, queue, webhook handler, or SMTP configuration. The application owns delivery policy and can replace its provider without changing template authoring or rendering.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Render outside Nitro"
|
|
3
|
+
description: "Compile the same Vue templates for a Node backend without booting Nuxt."
|
|
4
|
+
url: "https://nuxt-email.lupinum.com/docs/guides/standalone-rendering"
|
|
5
|
+
route: "/docs/guides/standalone-rendering"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Render outside Nitro
|
|
13
|
+
|
|
14
|
+
> Compile the same Vue templates for a Node backend without booting Nuxt.
|
|
15
|
+
|
|
16
|
+
Build a server-only email registry before you deploy your backend. The registry
|
|
17
|
+
uses the same rendering core as Nitro and the development preview. Your
|
|
18
|
+
application still owns delivery.
|
|
19
|
+
|
|
20
|
+
## Compile the registry
|
|
21
|
+
|
|
22
|
+
The output directory belongs to the build. Do not put source files there. The
|
|
23
|
+
helper rejects an existing directory that it did not create. Do not import the
|
|
24
|
+
generated registry into browser code.
|
|
25
|
+
|
|
26
|
+
```js [scripts/build-emails.mjs]
|
|
27
|
+
import { buildEmailRegistry } from '@lupinum/nuxt-email/build'
|
|
28
|
+
|
|
29
|
+
await buildEmailRegistry({
|
|
30
|
+
rootDir: '.',
|
|
31
|
+
outDir: 'generated/emails',
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Run this script from the application root before the backend build. Run it again
|
|
36
|
+
when templates or their imports change. Do not run it inside a request, action,
|
|
37
|
+
or background delivery job.
|
|
38
|
+
|
|
39
|
+
`rootDir` is the project root. Templates default to `app/emails`. For a custom
|
|
40
|
+
Nuxt `srcDir`, pass `templatesDir: 'src/emails'`, relative to `rootDir`. The helper
|
|
41
|
+
does not load Nuxt configuration. Relative `outDir` paths also use `rootDir`.
|
|
42
|
+
The output must remain inside the project root, outside the template directory,
|
|
43
|
+
so build-time declarations and the generated module can resolve project dependencies.
|
|
44
|
+
|
|
45
|
+
The helper generates:
|
|
46
|
+
|
|
47
|
+
- `generated/emails/index.mjs`: compiled Vue components and the runtime registry.
|
|
48
|
+
- `generated/emails/index.d.mts`: template names and inferred prop types.
|
|
49
|
+
- `generated/emails/types/`: Vue-generated declarations and their local imports.
|
|
50
|
+
|
|
51
|
+
These files are derived output. Keep them out of version control and regenerate
|
|
52
|
+
them in the deployment build. Ordinary TypeScript reads the declarations without
|
|
53
|
+
a Vue compiler. A compilation failure leaves the previous successful output
|
|
54
|
+
unchanged. Do not run concurrent builds against the same output path.
|
|
55
|
+
|
|
56
|
+
## Render with runtime data
|
|
57
|
+
|
|
58
|
+
```ts [backend/render-welcome.ts]
|
|
59
|
+
import { renderEmail } from '../generated/emails/index.mjs'
|
|
60
|
+
|
|
61
|
+
const email = await renderEmail('welcome', {
|
|
62
|
+
firstName: 'Ada',
|
|
63
|
+
activationUrl: 'https://example.com/activate',
|
|
64
|
+
})
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The template controls the props. This example uses the welcome template from the
|
|
68
|
+
quickstart. The result contains `html`, `text`, and an optional `subject`.
|
|
69
|
+
Validate external input before calling the renderer.
|
|
70
|
+
|
|
71
|
+
If your build already compiles Vue components, use the production renderer
|
|
72
|
+
directly:
|
|
73
|
+
|
|
74
|
+
```ts [backend/render-component.ts]
|
|
75
|
+
import { renderEmailComponent } from '@lupinum/nuxt-email/render'
|
|
76
|
+
import Welcome from './compiled/welcome.js'
|
|
77
|
+
|
|
78
|
+
const email = await renderEmailComponent(Welcome, {
|
|
79
|
+
firstName: 'Ada',
|
|
80
|
+
activationUrl: 'https://example.com/activate',
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Keep templates portable
|
|
85
|
+
|
|
86
|
+
Keep local imports inside the email directory. Import Vue helpers and
|
|
87
|
+
`defineEmail` explicitly. Nuxt aliases, plugins, auto-imported composables and
|
|
88
|
+
application state are not available in this build. The existing email components
|
|
89
|
+
remain registered by the renderer. Put reusable components in `components/` and
|
|
90
|
+
import them explicitly from templates.
|
|
91
|
+
|
|
92
|
+
The standalone compiler supports HTML templates and JavaScript or TypeScript
|
|
93
|
+
scripts, including `script setup`, async setup, and template-only components.
|
|
94
|
+
It rejects SFC `style` blocks, custom blocks, external blocks (`src`), JSX and
|
|
95
|
+
template preprocessors. Use `ETailwind` or inline styles. Nitro keeps its existing
|
|
96
|
+
Vue compiler integration; this portable subset does not replace that integration
|
|
97
|
+
or claim support for every feature in a Nuxt application.
|
|
98
|
+
|
|
99
|
+
This first standalone build does not configure `ECodeBlock`. Use Nitro for
|
|
100
|
+
templates that require configured syntax highlighting. There is no silent
|
|
101
|
+
fallback renderer. Preview fixtures are not discovered or imported by the
|
|
102
|
+
production registry.
|
|
103
|
+
|
|
104
|
+
The build uses Vue's SFC compiler, esbuild, TypeScript and vue-tsc. They are build dependencies of
|
|
105
|
+
the package, not imports of the production render entry. The runtime registry
|
|
106
|
+
leaves package dependencies external; the application's backend bundler must
|
|
107
|
+
include or resolve them.
|
|
108
|
+
|
|
109
|
+
## Verify the deployment
|
|
110
|
+
|
|
111
|
+
Node package rendering and generated declaration checks are separate from a
|
|
112
|
+
hosted deployment test. Convex Node actions are a target for integration testing;
|
|
113
|
+
this package change does not certify a deployed Convex application. The default
|
|
114
|
+
Convex runtime and edge workers are not supported claims.
|
|
115
|
+
|
|
116
|
+
Before production use, test the packed package in your actual backend, including
|
|
117
|
+
concurrent renders, dynamic props, fonts and Tailwind output. Measure cold and
|
|
118
|
+
warm execution. Never make auth delivery depend on an unverified rendering path.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Subject lines"
|
|
3
|
+
description: "Compute a subject line from the same typed props with defineEmail, and read it back from the render result."
|
|
4
|
+
url: "https://nuxt-email.lupinum.com/docs/guides/subject-lines"
|
|
5
|
+
route: "/docs/guides/subject-lines"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Subject lines
|
|
13
|
+
|
|
14
|
+
> Compute a subject line from the same typed props with defineEmail, and read it back from the render result.
|
|
15
|
+
|
|
16
|
+
Nuxt Email does not own subjects — sending is your provider's job — but a template often knows how to phrase its own subject from the same data it renders. `defineEmail` lets a template declare a subject and, when needed, authored plain text beside the template.
|
|
17
|
+
|
|
18
|
+
## Declare a subject
|
|
19
|
+
|
|
20
|
+
Call `defineEmail` in the template's `<script setup>`. It is an explicit server-only import from `@lupinum/nuxt-email/define-email`.
|
|
21
|
+
|
|
22
|
+
```vue [app/emails/welcome.vue]
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { defineEmail } from '@lupinum/nuxt-email/define-email'
|
|
25
|
+
|
|
26
|
+
const props = defineProps<{
|
|
27
|
+
firstName: string
|
|
28
|
+
activationUrl: string
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
defineEmail({
|
|
32
|
+
subject: () => `Welcome aboard, ${props.firstName}`,
|
|
33
|
+
})
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<EHtml lang="en">
|
|
38
|
+
<EHead><title>Activate your account</title></EHead>
|
|
39
|
+
<EBody>
|
|
40
|
+
<EPreview>Your account is ready.</EPreview>
|
|
41
|
+
<EContainer>
|
|
42
|
+
<EHeading>Welcome, {{ firstName }}</EHeading>
|
|
43
|
+
<EButton :href="activationUrl" :style="{ padding: '12px 20px' }">Activate</EButton>
|
|
44
|
+
</EContainer>
|
|
45
|
+
</EBody>
|
|
46
|
+
</EHtml>
|
|
47
|
+
</template>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The zero-argument `subject` closure captures the template's real `defineProps()` value and returns the subject string. Constant strings work too. There is no separate generic prop declaration that can drift from the `.vue` file.
|
|
51
|
+
|
|
52
|
+
To replace automatic plain-text derivation, add `text` to the same declaration:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
defineEmail({
|
|
56
|
+
subject: () => `Welcome aboard, ${props.firstName}`,
|
|
57
|
+
text: () => `Welcome, ${props.firstName}. Activate your account: ${props.activationUrl}`,
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`defineEmail` requires at least one of `subject` or `text`. Both accept a string or a synchronous function returning a string.
|
|
62
|
+
|
|
63
|
+
## Read it back
|
|
64
|
+
|
|
65
|
+
When a template declares `subject` with `defineEmail`, the render result carries it:
|
|
66
|
+
|
|
67
|
+
```ts [server/api/welcome.get.ts]
|
|
68
|
+
export default defineEventHandler(async () => {
|
|
69
|
+
const { html, text, subject } = await renderEmail('welcome', {
|
|
70
|
+
firstName: 'Ada',
|
|
71
|
+
activationUrl: 'https://example.com/activate',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// subject === 'Welcome aboard, Ada'
|
|
75
|
+
return { html, text, subject }
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`subject` is `string | undefined` on `RenderedEmail`. It is absent when the template does not declare `subject`, including when `defineEmail` declares only `text`.
|
|
80
|
+
|
|
81
|
+
## Rules and edge cases
|
|
82
|
+
|
|
83
|
+
- **Runs during render only.** Calling `defineEmail` outside an email render throws `DefineEmailOutsideRenderError`.
|
|
84
|
+
- **One declaration per render.** Calling it twice in one template render throws `DuplicateEmailDefinitionError`.
|
|
85
|
+
- **Returns strings synchronously.** A non-string subject or text value fails the render with an `EmailRenderError` whose cause explains the invalid value. Promise-returning functions are rejected by TypeScript.
|
|
86
|
+
- **Works before or after `await`.** The render context is propagated with `AsyncLocalStorage`, so declaring the subject after a top-level `await` in `<script setup>` (for example after fetching data) still resolves to the correct render.
|
|
87
|
+
- **Concurrent renders are isolated.** Each render has its own context, so subjects never leak between simultaneous renders.
|
|
88
|
+
|
|
89
|
+
<card title="You still pass it to your provider" icon="lucide:info">
|
|
90
|
+
`subject` is a convenience for keeping subject phrasing beside the template. You still hand it to your provider SDK alongside `from`, `to`, `html`, and `text` — Nuxt Email does not send mail or set headers.
|
|
91
|
+
</card>
|
|
92
|
+
|
|
93
|
+
Continue with the [sending guide](/raw/docs/guides/sending-email.md) for a complete provider handoff.
|
|
94
|
+
|
|
95
|
+
See the [`defineEmail` reference](/raw/docs/reference/define-email.md) for its exact signature, exports, precedence, and error contract.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Using Tailwind"
|
|
3
|
+
description: "How ETailwind inlines utilities, how nested-component classes are reached, and the exact documented limitations."
|
|
4
|
+
url: "https://nuxt-email.lupinum.com/docs/guides/tailwind"
|
|
5
|
+
route: "/docs/guides/tailwind"
|
|
6
|
+
locale: "en"
|
|
7
|
+
section: "Documentation"
|
|
8
|
+
collection: "docs"
|
|
9
|
+
source: "ginko-content"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Using Tailwind
|
|
13
|
+
|
|
14
|
+
> How ETailwind inlines utilities, how nested-component classes are reached, and the exact documented limitations.
|
|
15
|
+
|
|
16
|
+
Wrap an email document in `<ETailwind>` and Tailwind v4 utility classes on descendant elements are compiled into email-safe output at render time. Compatible declarations become inline styles; the boundary is server-only and requires a `<head>` inside it.
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<ETailwind>
|
|
20
|
+
<EHtml lang="en">
|
|
21
|
+
<EHead />
|
|
22
|
+
<EBody class="bg-gray-100">
|
|
23
|
+
<EContainer class="p-4">
|
|
24
|
+
<EText class="text-red-500">Styled with Tailwind</EText>
|
|
25
|
+
</EContainer>
|
|
26
|
+
</EBody>
|
|
27
|
+
</EHtml>
|
|
28
|
+
</ETailwind>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Precedence and non-inlinable rules
|
|
32
|
+
|
|
33
|
+
Inlining precedence is **component defaults < Tailwind utilities < author `style`**. Media queries and pseudo-classes cannot become inline styles. Nuxt Email moves them to a `<style>` element in `<head>`. It sanitizes residual class names and preserves Outlook-specific (`mso-*`) properties. Rendering fails with the affected class names when these rules have no head inside the boundary.
|
|
34
|
+
|
|
35
|
+
Tailwind v4 emits nested CSS for variants such as `hover:` and responsive combinations. Nuxt Email recursively flattens that nesting and converts media range syntax before it places the CSS in the email. A Tailwind nesting shape that cannot be converted safely fails the render instead of leaving a raw `&` rule in the output.
|
|
36
|
+
|
|
37
|
+
## Dark mode
|
|
38
|
+
|
|
39
|
+
Keep the base colors readable on their own. Some email clients ignore dark-mode rules, and others transform colors without using your rules.
|
|
40
|
+
|
|
41
|
+
The `dark:` variant emits `@media (prefers-color-scheme: dark)` CSS in `<head>`. Treat it as progressive enhancement.
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<EBody class="bg-white text-slate-900 dark:bg-slate-900 dark:text-white">
|
|
45
|
+
<EText>Readable with or without dark-mode support.</EText>
|
|
46
|
+
</EBody>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Support varies across Gmail and Outlook versions. Check the current [Can I Email evidence for `prefers-color-scheme`](https://www.caniemail.com/features/css-at-media-prefers-color-scheme/) and test the clients your recipients use.
|
|
50
|
+
|
|
51
|
+
You can extend the engine through props:
|
|
52
|
+
|
|
53
|
+
- `config` — a Tailwind config (everything except `content`), matching React Email's `TailwindConfig`.
|
|
54
|
+
- `theme` — raw CSS appended to the `@theme` layer.
|
|
55
|
+
- `utility` — raw CSS appended to the utilities layer.
|
|
56
|
+
|
|
57
|
+
## Share tokens, not the Nuxt stylesheet
|
|
58
|
+
|
|
59
|
+
Email rendering does not load the Nuxt application's CSS bundle, and browser CSS variables cannot be relied on in email clients. A class such as `text-primary` must resolve to a concrete value while the email renders.
|
|
60
|
+
|
|
61
|
+
Keep one application-owned token source and feed its value into `ETailwind` explicitly:
|
|
62
|
+
|
|
63
|
+
```vue
|
|
64
|
+
<script setup lang="ts">
|
|
65
|
+
import { emailTheme } from '~/shared/email-theme'
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<ETailwind :theme="emailTheme">
|
|
70
|
+
<EHtml lang="en">
|
|
71
|
+
<EHead />
|
|
72
|
+
<EBody>
|
|
73
|
+
<EText class="text-primary">Concrete email color</EText>
|
|
74
|
+
</EBody>
|
|
75
|
+
</EHtml>
|
|
76
|
+
</ETailwind>
|
|
77
|
+
</template>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```ts [shared/email-theme.ts]
|
|
81
|
+
export const emailTheme = '@theme { --color-primary: #2563eb; }'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The same token module can generate or inform your Nuxt theme, but Nuxt Email does not add a second CSS discovery path or silently import application styles.
|
|
85
|
+
|
|
86
|
+
`ETailwind` accepts the `config`, `theme`, and `utility` values you pass directly. It does not resolve arbitrary CSS file imports or CSS `@plugin` module imports at render time; supply executable Tailwind plugins through `config`. This keeps production rendering independent of the application filesystem.
|
|
87
|
+
|
|
88
|
+
## Classes inside nested components
|
|
89
|
+
|
|
90
|
+
Classes written directly in the email template are resolved during rendering. Classes emitted inside a nested component are resolved too. A reusable card or button therefore uses the same Tailwind behavior as inline markup:
|
|
91
|
+
|
|
92
|
+
```vue [Card.vue]
|
|
93
|
+
<template>
|
|
94
|
+
<div class="bg-red-500 p-4 md:text-lg">
|
|
95
|
+
<EText class="m-0">Title</EText>
|
|
96
|
+
<EButton class="bg-blue-600 px-4 py-2" href="https://example.com">Open</EButton>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```vue [email template]
|
|
102
|
+
<ETailwind>
|
|
103
|
+
<EHtml lang="en">
|
|
104
|
+
<EHead />
|
|
105
|
+
<EBody>
|
|
106
|
+
<Card />
|
|
107
|
+
</EBody>
|
|
108
|
+
</EHtml>
|
|
109
|
+
</ETailwind>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Style-bearing primitives such as `EText` and `EButton` resolve their Tailwind classes before their email-specific layout work. Plain HTML and structural components such as `EHtml` are handled in a second pass over the rendered HTML. That pass changes only marked class targets. Outlook-specific conditional comments and every other byte stay unchanged.
|
|
113
|
+
|
|
114
|
+
Rules that cannot be inlined, such as `md:text-lg`, still reach the `<head>` style element. Rendering fails if those rules have no head inside the boundary.
|
|
115
|
+
|
|
116
|
+
## Documented limitations
|
|
117
|
+
|
|
118
|
+
<card title="Limitations" icon="lucide:alert-triangle">
|
|
119
|
+
- `ECodeInline`, `EMarkdown`, `EPreview`, and `EFont` do **not** treat a nested `class` as a Tailwind style target — their `class`/head semantics differ.
|
|
120
|
+
- Nested `<ETailwind>` boundaries are **not** a supported configuration.
|
|
121
|
+
- Arbitrary CSS imports and CSS `@plugin` module imports are **not** resolved during rendering; executable plugins belong in `config`.
|
|
122
|
+
- Responsive or pseudo-class padding on `EContainer`, `ESection`, or `ERow` remains a CSS rule on the table; use an inner `EColumn` when that padding must survive clients that force collapsed table borders.
|
|
123
|
+
- Animation utilities include only the keyframes used by that render. Treat animation as best-effort progressive enhancement: [estimated email-client animation support is limited](https://www.caniemail.com/features/css-animation/), so the message must remain understandable when animation does not run.
|
|
124
|
+
- CSS nesting is downleveled because [estimated native nesting support in email clients is low](https://www.caniemail.com/features/css-nesting/). No raw nesting is intentionally emitted.
|
|
125
|
+
- Output tracks the fixed Tailwind version compiled by the engine.
|
|
126
|
+
</card>
|
|
127
|
+
|
|
128
|
+
Emails without `<ETailwind>` do not run the Tailwind path. The conformance cases cover basic inlining, media queries, Outlook-specific properties, nested components, and residual class sanitization. See the [conformance report](/raw/docs/reference/conformance.md).
|