N
Nuxt9mo ago
Yassaaa

Need help with creating mail templates with dynamic data and sending it

Summary: My goal is to send order confirmation emails using Nodemailer and Vue for email templates. I have successfully managed to: 1. Send emails using Nodemailer with SMTP. 2. Create email templates using Vue-mail but I did not use nuxt components? im not sure I am new to this nuxt module. Though I was able to use Tailwind in the email template. However, I am encountering issues when trying to combine these functionalities.
9 Replies
Yassaaa
YassaaaOP9mo ago
What I have tried: 1. Sending Emails with Nodemailer: I have a working implementation that sends emails using Nodemailer. Here is the relevant code:
<template>
<button @click="placeOrder" class="btn btn-circle">
Send Mail
</button>
</template>

<script setup lang="ts">
async function placeOrder() {
const orderDetails = {
senderEmail: '[email protected]',
recepient: '[email protected]',
subject: 'New Order Received',
name: 'TEST ORDER NOT REAL', // Replace with actual data
address: '123 Main St', // Replace with actual data
items: ['item1', 'item2'], // Replace with actual data
coupon: 'SAVE20' // Replace with actual data
};

try {
const response = await $fetch('/api/send-email', {
method: 'POST',
body: orderDetails,
});
alert('Email sent successfully!');
} catch (error) {
alert('An error occurred while sending the email. Please try again later.');
}
}

</script>
<template>
<button @click="placeOrder" class="btn btn-circle">
Send Mail
</button>
</template>

<script setup lang="ts">
async function placeOrder() {
const orderDetails = {
senderEmail: '[email protected]',
recepient: '[email protected]',
subject: 'New Order Received',
name: 'TEST ORDER NOT REAL', // Replace with actual data
address: '123 Main St', // Replace with actual data
items: ['item1', 'item2'], // Replace with actual data
coupon: 'SAVE20' // Replace with actual data
};

try {
const response = await $fetch('/api/send-email', {
method: 'POST',
body: orderDetails,
});
alert('Email sent successfully!');
} catch (error) {
alert('An error occurred while sending the email. Please try again later.');
}
}

</script>
Yassaaa
YassaaaOP9mo ago
2. Creating Email Templates with Vue: I have created a template using Vue components and props. Here is the template:
Yassaaa
YassaaaOP9mo ago
3. Combining Both: I tried to combine both functionalities by creating an API endpoint that generates the email HTML and sends the email. Here is the code:
import { defineEventHandler } from 'h3'
import { useShoppingCartStore } from '~/stores/shoppingCartStore'
import { useCheckoutStore } from '~/stores/checkoutStore'
import OrderConfirmationEmail from '~/emails/Order_Confirmation.vue'
import { renderToString } from '@vue/server-renderer'
import { createSSRApp, h } from 'vue'

export default defineEventHandler(async () => {
const shoppingCartStore = useShoppingCartStore()
const checkoutStore = useCheckoutStore()

const emailData = {
items: shoppingCartStore.items.map(item => ({
id: item._uid,
name: item.product_title.length > 32 ? item.product_title.slice(0, 31) + '...' : item.product_title,
quantity: item.item_count,
price: item.price,
image: item.product_image[0].filename || '/icon.svg'
})),
subtotal: shoppingCartStore.subtotal,
discount: shoppingCartStore.discount,
totalItems: shoppingCartStore.totalItems,
orderTotal: shoppingCartStore.total,
orderDate: new Date().toLocaleDateString(),
savings: shoppingCartStore.discount.toFixed(2),
shippingName: checkoutStore.formData.name,
shippingEmail: checkoutStore.formData.email,
shippingPhone: checkoutStore.formData.phone,
shippingAddress: checkoutStore.formData.address,
shippingCity: checkoutStore.formData.city,
shippingState: checkoutStore.formData.country
}

const app = createSSRApp({
render: () => h(OrderConfirmationEmail, { ...emailData })
})

const emailHtml = await renderToString(app)

return {
html: emailHtml
}
})
import { defineEventHandler } from 'h3'
import { useShoppingCartStore } from '~/stores/shoppingCartStore'
import { useCheckoutStore } from '~/stores/checkoutStore'
import OrderConfirmationEmail from '~/emails/Order_Confirmation.vue'
import { renderToString } from '@vue/server-renderer'
import { createSSRApp, h } from 'vue'

export default defineEventHandler(async () => {
const shoppingCartStore = useShoppingCartStore()
const checkoutStore = useCheckoutStore()

const emailData = {
items: shoppingCartStore.items.map(item => ({
id: item._uid,
name: item.product_title.length > 32 ? item.product_title.slice(0, 31) + '...' : item.product_title,
quantity: item.item_count,
price: item.price,
image: item.product_image[0].filename || '/icon.svg'
})),
subtotal: shoppingCartStore.subtotal,
discount: shoppingCartStore.discount,
totalItems: shoppingCartStore.totalItems,
orderTotal: shoppingCartStore.total,
orderDate: new Date().toLocaleDateString(),
savings: shoppingCartStore.discount.toFixed(2),
shippingName: checkoutStore.formData.name,
shippingEmail: checkoutStore.formData.email,
shippingPhone: checkoutStore.formData.phone,
shippingAddress: checkoutStore.formData.address,
shippingCity: checkoutStore.formData.city,
shippingState: checkoutStore.formData.country
}

const app = createSSRApp({
render: () => h(OrderConfirmationEmail, { ...emailData })
})

const emailHtml = await renderToString(app)

return {
html: emailHtml
}
})
Issues Encountered: 1. When trying to display the email HTML in the page's body, I get warnings like "missing required prop". 2. In the server console, I get the following error:
WARN [plugin inject] emails/Order_Confirmation.vue: rollup-plugin-inject: [...]. Consider restricting the plugin to particular files via options.include

ERROR RollupError: emails/Order_Confirmation.vue (1:0): Expression expected (Note that you need plugins to import files that are not JavaScript) nitro 10:56:21


1: <script setup lang="ts">
^
2: import {
3: Html,
WARN [plugin inject] emails/Order_Confirmation.vue: rollup-plugin-inject: [...]. Consider restricting the plugin to particular files via options.include

ERROR RollupError: emails/Order_Confirmation.vue (1:0): Expression expected (Note that you need plugins to import files that are not JavaScript) nitro 10:56:21


1: <script setup lang="ts">
^
2: import {
3: Html,
I am using Pinia for state management and want to pass the data from the Pinia states to the email template and send it with Nodemailer. The checkout store has a formData state that contains all the relevant data about the customer like name, email, phone, address, etc. I would appreciate any guidance or suggestions on how to resolve these issues and successfully combine sending emails with Nodemailer and creating email templates with Vue-Mail. I did use Claude and Github Copilot for help.
The Pegasus
The Pegasus3w ago
I'm just running into this. Did you ever manage to fix this issue? I tried adding this to nuxt.config.ts but I still get the rollup error...
import vuePlugin from "rollup-plugin-vue";

export default defineNuxtConfig({
// ...
nitro: {
rollupConfig: {
plugins: [
vuePlugin({
include: /\.vue$/,
target: "node",
}),
],
},
},
}
import vuePlugin from "rollup-plugin-vue";

export default defineNuxtConfig({
// ...
nitro: {
rollupConfig: {
plugins: [
vuePlugin({
include: /\.vue$/,
target: "node",
}),
],
},
},
}
@Yassaaa
Yassaaa
YassaaaOP3w ago
I dont think that you could use nuxt components in mail tempaltes because those use pure html+css. what I ended up doing in another project was in a utils folder I created: - emailSections.ts - emailStyles.ts - emailTemplate.ts here is the code in order:
import { styles } from './emailStyles';

export function renderMessageHeader() {
return `<h2 style="${styles.header}">Neue Anfrage erhalten</h2>`;
}

export function renderMessageInfo(name: string, email: string, service: string) {
return `
<div style="${styles.info}">
<p><strong>Name:</strong> ${name}</p>
<p><strong>E-mail:</strong> ${email}</p>
<p><strong>Dienstleistung:</strong> ${service}</p>
</div>
`;
}

export function renderMessageBody(subject: string, message: string) {
return `
<div style="${styles.body}">
<div style="${styles.subject}">Betreff: ${subject}</div>
<p style="${styles.message}">${message}</p>
</div>
`;
}
import { styles } from './emailStyles';

export function renderMessageHeader() {
return `<h2 style="${styles.header}">Neue Anfrage erhalten</h2>`;
}

export function renderMessageInfo(name: string, email: string, service: string) {
return `
<div style="${styles.info}">
<p><strong>Name:</strong> ${name}</p>
<p><strong>E-mail:</strong> ${email}</p>
<p><strong>Dienstleistung:</strong> ${service}</p>
</div>
`;
}

export function renderMessageBody(subject: string, message: string) {
return `
<div style="${styles.body}">
<div style="${styles.subject}">Betreff: ${subject}</div>
<p style="${styles.message}">${message}</p>
</div>
`;
}
// Reusable email-safe styles with mobile responsiveness
export const styles = {
container: `
min-height: 100vh;
padding: 5%;
background-color: #fff8ef;
@media (max-width: 768px) {
padding: 20px;
}
`,
wrapper: `
border: 4px solid #de943d;
background-color: #fff8ef;
padding: 40px;
height: 100%;
display: flex;
flex-direction: column;
gap: 20px;
box-sizing: border-box;
@media (max-width: 768px) {
padding: 20px;
gap: 15px;
}
`,
// Using a mobile-first approach for the header
header: `
text-align: center;
font-size: 24px !important; /* Base size for mobile */
font-weight: bold;
text-decoration: underline;
text-underline-offset: 4px;
color: black;
max-width: 100%;
@media (min-width: 768px) {
font-size: 38px !important; /* Larger size for desktop */
}
`,
info: `
display: flex;
padding: 20px;
flex-direction: column;
gap: 10px;
align-self: start;
font-size: 1.1rem;
color: black;
width: 100%;
box-sizing: border-box;
@media (max-width: 768px) {
padding: 15px;
font-size: 1rem;
gap: 8px;
}
`,
body: `
background-color: white;
padding: 20px;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 20px;
color: black;
width: 100%;
box-sizing: border-box;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.06));
@media (max-width: 768px) {
padding: 15px;
gap: 15px;
}
`,
subject: `
font-size: 1.1rem;
font-weight: bold;
padding-bottom: 8px;
border-bottom: 4px solid #de943d;
@media (max-width: 768px) {
font-size: 1rem;
padding-bottom: 6px;
border-bottom-width: 3px;
}
`,
message: `
white-space: pre-wrap;
flex-grow: 1;
word-break: break-word;
`,
};
// Reusable email-safe styles with mobile responsiveness
export const styles = {
container: `
min-height: 100vh;
padding: 5%;
background-color: #fff8ef;
@media (max-width: 768px) {
padding: 20px;
}
`,
wrapper: `
border: 4px solid #de943d;
background-color: #fff8ef;
padding: 40px;
height: 100%;
display: flex;
flex-direction: column;
gap: 20px;
box-sizing: border-box;
@media (max-width: 768px) {
padding: 20px;
gap: 15px;
}
`,
// Using a mobile-first approach for the header
header: `
text-align: center;
font-size: 24px !important; /* Base size for mobile */
font-weight: bold;
text-decoration: underline;
text-underline-offset: 4px;
color: black;
max-width: 100%;
@media (min-width: 768px) {
font-size: 38px !important; /* Larger size for desktop */
}
`,
info: `
display: flex;
padding: 20px;
flex-direction: column;
gap: 10px;
align-self: start;
font-size: 1.1rem;
color: black;
width: 100%;
box-sizing: border-box;
@media (max-width: 768px) {
padding: 15px;
font-size: 1rem;
gap: 8px;
}
`,
body: `
background-color: white;
padding: 20px;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 20px;
color: black;
width: 100%;
box-sizing: border-box;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.06));
@media (max-width: 768px) {
padding: 15px;
gap: 15px;
}
`,
subject: `
font-size: 1.1rem;
font-weight: bold;
padding-bottom: 8px;
border-bottom: 4px solid #de943d;
@media (max-width: 768px) {
font-size: 1rem;
padding-bottom: 6px;
border-bottom-width: 3px;
}
`,
message: `
white-space: pre-wrap;
flex-grow: 1;
word-break: break-word;
`,
};
import { styles } from './emailStyles';
import { renderMessageHeader, renderMessageInfo, renderMessageBody } from './emailSections';

export function generateEmailTemplate({
name,
email,
service,
subject,
message,
}: {
name: string;
email: string;
service: string;
subject: string;
message: string;
}) {
return `
<div style="${styles.container}">
<div style="${styles.wrapper}">
${renderMessageHeader()}
${renderMessageInfo(name, email, service)}
${renderMessageBody(subject, message)}
</div>
</div>
`;
}
import { styles } from './emailStyles';
import { renderMessageHeader, renderMessageInfo, renderMessageBody } from './emailSections';

export function generateEmailTemplate({
name,
email,
service,
subject,
message,
}: {
name: string;
email: string;
service: string;
subject: string;
message: string;
}) {
return `
<div style="${styles.container}">
<div style="${styles.wrapper}">
${renderMessageHeader()}
${renderMessageInfo(name, email, service)}
${renderMessageBody(subject, message)}
</div>
</div>
`;
}
then I created a contact.post.ts in server/api/ code:
import { generateEmailTemplate } from '../../utils/emailTemplate';

export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { sendMail } = useNodeMailer();

const mailOptions = {
to: '[email protected]', // you need to use your desired email that you want to send to (for my case was just contact form emails that I receive so I was fine with sending it to one of my emails
subject: body.subject,
html: generateEmailTemplate(body),
replyTo: body.email,
};

try {
await sendMail(mailOptions);
return { success: true, message: 'E-Mail erfolgreich gesendet.' };
} catch (error) {
console.error('Fehler beim Senden der E-Mail:', error);
return { success: false, message: 'E-Mail konnte nicht gesendet werden.' };
}
});
import { generateEmailTemplate } from '../../utils/emailTemplate';

export default defineEventHandler(async (event) => {
const body = await readBody(event);
const { sendMail } = useNodeMailer();

const mailOptions = {
to: '[email protected]', // you need to use your desired email that you want to send to (for my case was just contact form emails that I receive so I was fine with sending it to one of my emails
subject: body.subject,
html: generateEmailTemplate(body),
replyTo: body.email,
};

try {
await sendMail(mailOptions);
return { success: true, message: 'E-Mail erfolgreich gesendet.' };
} catch (error) {
console.error('Fehler beim Senden der E-Mail:', error);
return { success: false, message: 'E-Mail konnte nicht gesendet werden.' };
}
});
I hope this answers your question 🙂 If it was not clear I used nuxt-nodemailer module. so ofc you need to make sure you configured it so it can work. 👍 in nuxt.config.ts:
nodemailer: {
from: '"New Contact From Submission" <[email protected]>',
host: process.env.NUXT_nmHost,
port: Number(process.env.NUXT_nmPort),
secure: process.env.NUXT_nmSecure === 'true',
auth: {
user: process.env.NUXT_nmUser,
pass: process.env.NUXT_nmPass,
},
},
nodemailer: {
from: '"New Contact From Submission" <[email protected]>',
host: process.env.NUXT_nmHost,
port: Number(process.env.NUXT_nmPort),
secure: process.env.NUXT_nmSecure === 'true',
auth: {
user: process.env.NUXT_nmUser,
pass: process.env.NUXT_nmPass,
},
},
The Pegasus
The Pegasus3w ago
Actually, I managed to make it work!
import vue from "@vitejs/plugin-vue";

export default defineNuxtConfig({
// ...
nitro: {
rollupConfig: {
plugins: [vue()],
},
},
}
import vue from "@vitejs/plugin-vue";

export default defineNuxtConfig({
// ...
nitro: {
rollupConfig: {
plugins: [vue()],
},
},
}
This does the trick. I had two nitro objects in my config, so this plugin was getting overwritten. @Yassaaa I am aware not everything Vue does is available on an email, but you definitely can use it as a templating engine.
Mak
Mak3w ago
Nuxt
nuxt-mail · Nuxt Modules
Adds email sending capability to a Nuxt.js app. Adds a server route, an injected variable, and uses nodemailer to send emails.
Mak
Mak3w ago
The Pegasus
The Pegasus3w ago
vue-email seems interesting as I am using Resend to send emails and it is "inspired by the Resend team's React Email library". Thanks, @Mak.

Did you find this page helpful?