nuxt-generation-emails 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 treygrr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,373 @@
1
+ # nuxt-generation-emails
2
+
3
+ [![License][license-src]][license-href]
4
+ [![Nuxt][nuxt-src]][nuxt-href]
5
+
6
+ A Nuxt module for authoring, previewing, and sending transactional email templates — all from inside your Nuxt app.
7
+
8
+ ## ✨ Features
9
+
10
+ - 📧 **Vue-powered email templates** — Build emails with `@vue-email/components` and Tailwind CSS
11
+ - 🔌 **Auto-generated API routes** — Every template gets a `POST /api/emails/...` endpoint automatically
12
+ - 🖥️ **Live preview UI** — Browse and tweak templates at `/__emails/` with a built-in props editor
13
+ - 🛠️ **CLI scaffolding** — `nuxt-gen-emails add` creates templates with the right structure instantly
14
+ - 🔄 **Hot reload** — New templates are detected automatically during dev (server restarts to register routes)
15
+ - 📋 **OpenAPI docs** — Generated routes include full OpenAPI metadata out of the box
16
+ - 🔗 **Shareable URLs** — Share template previews with pre-filled prop values via URL params
17
+
18
+ ---
19
+
20
+ ## 📦 1. Installation
21
+
22
+ Install the module in your Nuxt 4+ project:
23
+
24
+ ```bash
25
+ npm install nuxt-generation-emails
26
+ ```
27
+
28
+ Then add it to your `nuxt.config.ts`:
29
+
30
+ ```ts
31
+ export default defineNuxtConfig({
32
+ modules: ['nuxt-generation-emails'],
33
+
34
+ nuxtGenerationEmails: {
35
+ // Directory for email templates (relative to your app's srcDir)
36
+ emailDir: 'emails', // default
37
+ },
38
+ })
39
+ ```
40
+
41
+ > **Tip:** Enable Nitro's OpenAPI support to get auto-generated API docs for every email endpoint:
42
+ >
43
+ > ```ts
44
+ > nitro: {
45
+ > experimental: {
46
+ > openAPI: true,
47
+ > },
48
+ > },
49
+ > ```
50
+
51
+ ---
52
+
53
+ ## 📁 2. Folder Structure
54
+
55
+ Templates live inside your app's source directory under the configured `emailDir` (default: `emails/`). You can nest them in subdirectories to organize by version, category, or however you like.
56
+
57
+ ```
58
+ app/
59
+ emails/
60
+ v1/
61
+ order-confirmation.vue
62
+ welcome.vue
63
+ v2/
64
+ order-confirmation.vue
65
+ marketing/
66
+ promo.vue
67
+ ```
68
+
69
+ The directory structure maps directly to routes:
70
+
71
+ | Template file | Preview URL | API endpoint |
72
+ |----------------------------------------|-------------------------------------------|------------------------------------------|
73
+ | `emails/v1/order-confirmation.vue` | `/__emails/v1/order-confirmation` | `POST /api/emails/v1/order-confirmation` |
74
+ | `emails/v1/welcome.vue` | `/__emails/v1/welcome` | `POST /api/emails/v1/welcome` |
75
+ | `emails/v2/order-confirmation.vue` | `/__emails/v2/order-confirmation` | `POST /api/emails/v2/order-confirmation` |
76
+ | `emails/marketing/promo.vue` | `/__emails/marketing/promo` | `POST /api/emails/marketing/promo` |
77
+
78
+ ---
79
+
80
+ ## 🛠️ 3. Adding Templates with the CLI
81
+
82
+ The fastest way to create a new email template:
83
+
84
+ ```bash
85
+ npx nuxt-gen-emails add <name>
86
+ ```
87
+
88
+ ### Basic usage
89
+
90
+ ```bash
91
+ # Creates emails/welcome.vue
92
+ npx nuxt-gen-emails add welcome
93
+
94
+ # Creates emails/v1/order-confirmation.vue (creates v1/ if it doesn't exist)
95
+ npx nuxt-gen-emails add v1/order-confirmation
96
+
97
+ # Creates emails/marketing/campaigns/summer-sale.vue (deeply nested)
98
+ npx nuxt-gen-emails add marketing/campaigns/summer-sale
99
+ ```
100
+
101
+ ### Interactive directory selection
102
+
103
+ If you run the command without a path prefix, and directories already exist, the CLI will ask if you want to place the template in an existing directory:
104
+
105
+ ```bash
106
+ npx nuxt-gen-emails add reset-password
107
+
108
+ # ? Would you like to select an existing directory? (y/N)
109
+ # ? Select a directory:
110
+ # > emails/ (root)
111
+ # emails/v1/
112
+ # emails/v2/
113
+ # emails/marketing/
114
+ ```
115
+
116
+ ### What gets generated
117
+
118
+ Every `add` command creates a single `.vue` file with a ready-to-customize starter template:
119
+
120
+ ```vue
121
+ <script setup lang="ts">
122
+ import { Body, Button, Font, Head, Hr, Html, Text, Tailwind } from '@vue-email/components'
123
+
124
+ defineOptions({ name: 'WelcomeNge' })
125
+
126
+ const props = withDefaults(defineProps<{
127
+ title?: string
128
+ message?: string
129
+ }>(), {
130
+ title: 'Welcome!',
131
+ message: 'This is the welcome email template.',
132
+ })
133
+ </script>
134
+
135
+ <template>
136
+ <Tailwind>
137
+ <Html lang="en">
138
+ <Head />
139
+ <Font
140
+ font-family="DM Sans"
141
+ :fallback-font-family="['Arial', 'Helvetica', 'sans-serif']"
142
+ :web-font="{ url: 'https://fonts.gstatic.com/s/dmsans/v15/rP2Hp2ywxg089UriCZOIHTWEBlw.woff2', format: 'woff2' }"
143
+ />
144
+ <Body style="font-family: 'DM Sans', Arial, Helvetica, sans-serif;">
145
+ <Text>{{ props.title }}</Text>
146
+ <p>{{ props.message }}</p>
147
+ <Hr />
148
+ <Button href="https://example.com">
149
+ Click me
150
+ </Button>
151
+ </Body>
152
+ </Html>
153
+ </Tailwind>
154
+ </template>
155
+ ```
156
+
157
+ If the dev server is running, it will automatically detect the new file and restart to register the new routes — no manual restart needed.
158
+
159
+ ---
160
+
161
+ ## ✍️ 4. Writing Email Templates
162
+
163
+ Templates are standard Vue SFCs using [`@vue-email/components`](https://vuemail.net/). Define your template's dynamic data using `defineProps` with `withDefaults`:
164
+
165
+ ```vue
166
+ <script setup lang="ts">
167
+ import { Body, Button, Container, Head, Heading, Html, Text, Tailwind } from '@vue-email/components'
168
+
169
+ defineOptions({ name: 'OrderConfirmationNge' })
170
+
171
+ const props = withDefaults(defineProps<{
172
+ customerName?: string
173
+ orderNumber?: string
174
+ orderDate?: string
175
+ totalAmount?: number
176
+ }>(), {
177
+ customerName: 'Customer',
178
+ orderNumber: 'ORD-000000',
179
+ orderDate: 'January 1, 2026',
180
+ totalAmount: 0,
181
+ })
182
+ </script>
183
+
184
+ <template>
185
+ <Tailwind>
186
+ <Html lang="en">
187
+ <Head />
188
+ <Body>
189
+ <Container>
190
+ <Heading as="h1">Order Confirmed!</Heading>
191
+ <Text>Hi {{ props.customerName }},</Text>
192
+ <Text>Your order #{{ props.orderNumber }} placed on {{ props.orderDate }} is confirmed.</Text>
193
+ <Text>Total: ${{ props.totalAmount?.toFixed(2) }}</Text>
194
+ <Button href="https://example.com/orders">View Order</Button>
195
+ </Container>
196
+ </Body>
197
+ </Html>
198
+ </Tailwind>
199
+ </template>
200
+ ```
201
+
202
+ ### Key rules
203
+
204
+ 1. **Use `withDefaults(defineProps<{...}>())`** — Props are extracted at build time to populate the preview UI and API example payloads
205
+ 2. **Make all props optional** (`?:`) — The defaults provide sensible preview values
206
+ 3. **Supported prop types** — `string`, `number`, `boolean` appear as editable fields in the preview UI. Complex types (objects, arrays) work fine but are only editable via the JSON editor
207
+
208
+ ---
209
+
210
+ ## 🖥️ 5. Using the Preview UI
211
+
212
+ Navigate to `/__emails/` in your browser during development to access the preview interface.
213
+
214
+ ### What you'll see
215
+
216
+ - **Template selector** — A dropdown at the top of the page lists all templates, organized by directory. Click a folder to expand/collapse it, click a template name to load it.
217
+ - **Props sidebar** — String and number props appear as editable input fields on the left. Changes update the preview in real time.
218
+ - **Live preview** — The rendered email is displayed on the right, exactly as it will look when sent.
219
+ - **Share URL button** — Copies a URL with the current prop values encoded as query parameters, useful for sharing specific test states with teammates.
220
+ - **API tester** — At the bottom of the sidebar, a built-in API tester lets you fire a real `POST` request to the template's endpoint. It shows the full JSON request body (editable) and the response including the rendered HTML.
221
+
222
+ ### Navigation
223
+
224
+ Every template is accessible at a direct URL matching its file path:
225
+
226
+ ```
227
+ http://localhost:3000/__emails/v1/order-confirmation
228
+ http://localhost:3000/__emails/v2/welcome
229
+ ```
230
+
231
+ ---
232
+
233
+ ## 📨 6. Wiring Up Email Sending (SendGrid Example)
234
+
235
+ The module generates `POST` endpoints for every template that render the email HTML. To actually **send** emails, provide a `sendGenEmails` function in your module config.
236
+
237
+ ### With SendGrid
238
+
239
+ Install the SendGrid SDK:
240
+
241
+ ```bash
242
+ npm install @sendgrid/mail
243
+ ```
244
+
245
+ Configure the handler in `nuxt.config.ts`:
246
+
247
+ ```ts
248
+ import sgMail from '@sendgrid/mail'
249
+
250
+ sgMail.setApiKey(process.env.SENDGRID_API_KEY!)
251
+
252
+ export default defineNuxtConfig({
253
+ modules: ['nuxt-generation-emails'],
254
+
255
+ nuxtGenerationEmails: {
256
+ emailDir: 'emails',
257
+
258
+ sendGenEmails: async (html, data) => {
259
+ await sgMail.send({
260
+ to: data.to as string,
261
+ from: 'noreply@yourdomain.com',
262
+ subject: data.subject as string || 'No Subject',
263
+ html,
264
+ })
265
+ },
266
+ },
267
+ })
268
+ ```
269
+
270
+ ### How it works
271
+
272
+ When a `POST` request hits an email endpoint (e.g., `/api/emails/v1/order-confirmation`):
273
+
274
+ 1. The request body is read and passed as props to the Vue email template
275
+ 2. `@vue-email/render` renders the template to an HTML string
276
+ 3. If `sendGenEmails` is configured, it's called with the rendered `html` and the original request `data`
277
+ 4. The response always returns `{ success: true, html: "..." }` so you can inspect the rendered output
278
+
279
+ ### Example API call
280
+
281
+ ```bash
282
+ curl -X POST http://localhost:3000/api/emails/v1/order-confirmation \
283
+ -H "Content-Type: application/json" \
284
+ -d '{
285
+ "customerName": "Jane Doe",
286
+ "orderNumber": "ORD-123456",
287
+ "orderDate": "February 17, 2026",
288
+ "totalAmount": 89.99,
289
+ "to": "jane@example.com",
290
+ "subject": "Your Order is Confirmed!"
291
+ }'
292
+ ```
293
+
294
+ ### Using the Nitro hook alternative
295
+
296
+ If you prefer not to configure the handler in `nuxt.config.ts`, you can listen for the `nuxt-gen-emails:send` hook in a Nitro plugin:
297
+
298
+ ```ts
299
+ // server/plugins/email-sender.ts
300
+ export default defineNitroPlugin((nitro) => {
301
+ nitro.hooks.hook('nuxt-gen-emails:send', async ({ html, data }) => {
302
+ // Your sending logic here
303
+ console.log('Sending email with data:', data)
304
+ })
305
+ })
306
+ ```
307
+
308
+ ---
309
+
310
+ ## 🔧 7. Module Options
311
+
312
+ | Option | Type | Default | Description |
313
+ |------------------|------------|------------|--------------------------------------------------------------------|
314
+ | `emailDir` | `string` | `'emails'` | Directory containing email templates (relative to `srcDir`) |
315
+ | `sendGenEmails` | `function` | `undefined`| Async function called with `(html, data)` when an email is sent |
316
+
317
+ Full config key: `nuxtGenerationEmails`
318
+
319
+ ---
320
+
321
+ ## 🧩 Auto-Imports
322
+
323
+ The module auto-imports these utilities for convenience:
324
+
325
+ ### Client-side
326
+
327
+ - `encodeStoreToUrlParams(store)` — Encode a props object into URL search parameters
328
+ - `generateShareableUrl(store)` — Generate a full shareable URL for the current template with encoded props
329
+
330
+ ### Server-side
331
+
332
+ - `getSendGenEmailsHandler()` — Retrieve the configured `sendGenEmails` function (used internally by generated routes)
333
+
334
+ ---
335
+
336
+ ## 🏗️ Development
337
+
338
+ <details>
339
+ <summary>Local development</summary>
340
+
341
+ ```bash
342
+ # Install dependencies
343
+ npm install
344
+
345
+ # Generate type stubs
346
+ npm run dev:prepare
347
+
348
+ # Develop with the playground
349
+ npm run dev
350
+
351
+ # Build the playground
352
+ npm run dev:build
353
+
354
+ # Run ESLint
355
+ npm run lint
356
+
357
+ # Run Vitest
358
+ npm run test
359
+ npm run test:watch
360
+ ```
361
+
362
+ </details>
363
+
364
+ ## License
365
+
366
+ [MIT](./LICENSE)
367
+
368
+ <!-- Badges -->
369
+ [license-src]: https://img.shields.io/npm/l/nuxt-generation-emails.svg?style=flat&colorA=020420&colorB=00DC82
370
+ [license-href]: https://npmjs.com/package/nuxt-generation-emails
371
+
372
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt
373
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ import { defineCommand, runMain } from 'citty';
3
+ import { join, relative } from 'pathe';
4
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'node:fs';
5
+ import { consola } from 'consola';
6
+ import { loadNuxtConfig } from '@nuxt/kit';
7
+
8
+ function generateVueTemplate(emailName) {
9
+ const capitalizedEmailName = emailName.charAt(0).toUpperCase() + emailName.slice(1);
10
+ const componentName = `${capitalizedEmailName}Nge`;
11
+ return `<script setup lang="ts">
12
+ import { Body, Button, Font, Head, Hr, Html, Text, Tailwind } from '@vue-email/components'
13
+
14
+ defineOptions({ name: '${componentName}' })
15
+
16
+ const props = withDefaults(defineProps<{
17
+ title?: string
18
+ message?: string
19
+ }>(), {
20
+ title: 'Welcome!',
21
+ message: 'This is the ${emailName} email template.',
22
+ })
23
+ <\/script>
24
+
25
+ <template>
26
+ <Tailwind>
27
+ <Html lang="en">
28
+ <Head />
29
+ <Font
30
+ font-family="DM Sans"
31
+ :fallback-font-family="['Arial', 'Helvetica', 'sans-serif']"
32
+ :web-font="{ url: 'https://fonts.gstatic.com/s/dmsans/v15/rP2Hp2ywxg089UriCZOIHTWEBlw.woff2', format: 'woff2' }"
33
+ />
34
+ <Body style="font-family: 'DM Sans', Arial, Helvetica, sans-serif;">
35
+ <Text>{{ props.title }}</Text>
36
+ <p>{{ props.message }}</p>
37
+ <Hr />
38
+ <Button href="https://example.com">
39
+ Click me
40
+ </Button>
41
+ </Body>
42
+ </Html>
43
+ </Tailwind>
44
+ </template>
45
+ `;
46
+ }
47
+
48
+ async function findEmailsDir() {
49
+ const cwd = process.cwd();
50
+ try {
51
+ const config = await loadNuxtConfig({ cwd });
52
+ const srcDir = config.srcDir || cwd;
53
+ return join(srcDir, "emails");
54
+ } catch {
55
+ if (existsSync(join(cwd, "app"))) {
56
+ return join(cwd, "app", "emails");
57
+ }
58
+ if (existsSync(join(cwd, "src"))) {
59
+ return join(cwd, "src", "emails");
60
+ }
61
+ return join(cwd, "emails");
62
+ }
63
+ }
64
+ function getAllDirectories(dirPath, basePath = dirPath) {
65
+ const dirs = [];
66
+ if (!existsSync(dirPath)) {
67
+ return dirs;
68
+ }
69
+ const entries = readdirSync(dirPath);
70
+ for (const entry of entries) {
71
+ const fullPath = join(dirPath, entry);
72
+ if (statSync(fullPath).isDirectory()) {
73
+ const relativePath = relative(basePath, fullPath);
74
+ dirs.push(relativePath);
75
+ dirs.push(...getAllDirectories(fullPath, basePath));
76
+ }
77
+ }
78
+ return dirs;
79
+ }
80
+ function parseTemplateName(name) {
81
+ const namePath = name.replace(/^\.\//, "").replace(/\.vue$/, "");
82
+ const parts = namePath.split("/");
83
+ const emailName = parts.pop();
84
+ const subDir = parts.length > 0 ? join(...parts) : "";
85
+ return { emailName, subDir };
86
+ }
87
+ async function promptForDirectory(emailsDir, initialSubDir, argsDir) {
88
+ if (initialSubDir || argsDir) {
89
+ return initialSubDir;
90
+ }
91
+ const existingDirs = getAllDirectories(emailsDir);
92
+ if (existingDirs.length === 0) {
93
+ return "";
94
+ }
95
+ const useExisting = await consola.prompt("Would you like to select an existing directory?", {
96
+ type: "confirm",
97
+ initial: false
98
+ });
99
+ if (!useExisting) {
100
+ return "";
101
+ }
102
+ const selectedDir = await consola.prompt("Select a directory:", {
103
+ type: "select",
104
+ options: [
105
+ { label: "emails/ (root)", value: "" },
106
+ ...existingDirs.map((dir) => ({ label: `emails/${dir}/`, value: dir }))
107
+ ]
108
+ });
109
+ return selectedDir;
110
+ }
111
+ function ensureDirectoryExists(dirPath, description) {
112
+ if (!existsSync(dirPath)) {
113
+ mkdirSync(dirPath, { recursive: true });
114
+ consola.success(`Created ${description}: ${dirPath}`);
115
+ }
116
+ }
117
+ function checkFileExists(filePath) {
118
+ if (existsSync(filePath)) {
119
+ consola.error(`Email template already exists: ${filePath}`);
120
+ process.exit(1);
121
+ }
122
+ }
123
+ function createEmailFiles(targetDir, emailName, emailPath) {
124
+ const vueFile = join(targetDir, `${emailName}.vue`);
125
+ checkFileExists(vueFile);
126
+ const vueTemplate = generateVueTemplate(emailName);
127
+ writeFileSync(vueFile, vueTemplate, "utf-8");
128
+ consola.success(`Created email template: ${vueFile}`);
129
+ return {
130
+ emailPath,
131
+ vueFile
132
+ };
133
+ }
134
+ const addCommand = defineCommand({
135
+ meta: {
136
+ name: "add",
137
+ description: "Scaffold a new email template"
138
+ },
139
+ args: {
140
+ name: {
141
+ type: "positional",
142
+ description: "Name of the email template to create",
143
+ required: true
144
+ },
145
+ dir: {
146
+ type: "string",
147
+ description: "Directory to create the email in (relative to emails folder)",
148
+ default: ""
149
+ }
150
+ },
151
+ async run({ args }) {
152
+ const emailsDir = await findEmailsDir();
153
+ const { emailName, subDir: initialSubDir } = parseTemplateName(args.name);
154
+ const subDir = await promptForDirectory(emailsDir, initialSubDir, args.dir);
155
+ const targetDir = join(emailsDir, args.dir, subDir);
156
+ ensureDirectoryExists(targetDir, "emails directory");
157
+ const emailPath = join(subDir, emailName).replace(/\\/g, "/");
158
+ createEmailFiles(targetDir, emailName, emailPath);
159
+ }
160
+ });
161
+
162
+ const main = defineCommand({
163
+ meta: {
164
+ name: "nuxt-gen-emails",
165
+ description: "CLI for nuxt-gen-emails module",
166
+ version: "1.0.0"
167
+ },
168
+ subCommands: {
169
+ add: addCommand
170
+ }
171
+ });
172
+ runMain(main);
@@ -0,0 +1,11 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ /** Directory containing email templates; resolved from srcDir when relative. */
5
+ emailDir?: string;
6
+ sendGenEmails?: (html: string, data: Record<string, unknown>) => Promise<void> | void;
7
+ }
8
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
9
+
10
+ export { _default as default };
11
+ export type { ModuleOptions };
@@ -0,0 +1,11 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ /** Directory containing email templates; resolved from srcDir when relative. */
5
+ emailDir?: string;
6
+ sendGenEmails?: (html: string, data: Record<string, unknown>) => Promise<void> | void;
7
+ }
8
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
9
+
10
+ export { _default as default };
11
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "nuxt-generation-emails",
3
+ "configKey": "nuxtGenerationEmails",
4
+ "compatibility": {
5
+ "nuxt": ">=4.0.0"
6
+ },
7
+ "version": "0.1.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "1.0.2",
10
+ "unbuild": "3.6.1"
11
+ }
12
+ }