Skip to content

Wedding invitation app in NuxtJS and Nodemailer

Creating email invites and sending them to your guest list can be an expensive and daunting task. In this article, we review how can leverage existing services to dynamically create wedding invites and send them to an invite list as easily as possible

The final project demo can be viewed on Codesandbox.

We are first going to create a new Nuxt.Js project. Nuxt.Js is an intuitive Vue.Js framework hailed for being modular performant and enjoyable.

In your desired work directory, open up your terminal of choice and run the following command:


yarn create nuxt-app nuxtjs-email-wedding-invitation

# OR

npx create-nuxt-app nuxtjs-email-wedding-invitation

# OR

npm init nuxt-app nuxtjs-email-wedding-invitation

Code language: PHP (php)

You will then receive a set of questions to help the installer configure your project. Here are our recommendations:

Project name: nuxtjs-email-wedding-invitation

Programming language: JavaScript

Package manager: Yarn

UI framework: Tailwind CSS

Nuxt.js modules: None

Linting tools: None

Testing framework: None

Rendering mode: Universal (SSR / SSG)

Deployment target: Server (Node.Js hosting)

Development tools: None

Version control: Git

You may now enter the project directory and run the project:


cd nuxtjs-email-wedding-invitation

  
yarn dev

#OR

npm run dev

Code language: CSS (css)

To dynamically create the invites, we are going to use Cloudinary as our platform of choice.

Cloudinary aims to help unleash media’s full potential in companies by proving a powerful media developer experience.

To install, we will use the nuxt/cloudinary package. Run the following command in the project directory:


yarn add @nuxtjs/cloudinary

# OR

npm install @nuxtjs/cloudinary

Code language: CSS (css)

We will then add @nuxtjs/cloudinary as a module in the modules section of nuxt.config.js:


// nuxt.config.js

  

export  default  {

...

modules:[

'@nuxtjs/cloudinary'

]

}

  

Code language: JavaScript (javascript)

To configure the module, we will create a cloudinary section in nuxt.config.js.


// nuxt.config.js

export  default{

...

cloudinary:  {

cloudName:  process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME,

secure:  true,

useComponent:  true

}

}

Code language: JavaScript (javascript)

The above code snippet utilizes environmental variables to retrieve the cloudName. Environmental variables are values we configure outside our codebase based on the environment our code is in. These can be sensitive details such as login credentials.

We are going to create a .env file at the root of our project to store our environmental variables. Nuxt.Js will automatically load these variables as long as they are prefixed with NUXT_ENV


cat .env

Code language: CSS (css)

<!-- .env -->

NUXT_ENV_CLOUDINARY_CLOUD_NAME = secret-cloud-name

Code language: HTML, XML (xml)

Change the secret-cloud-name to your Cloudinary cloud name. If you don’t have one, simply create an account on Cloudinary and refer to the console section.

We will be using some basic forms in this project. @tailwind/forms provides some basic styling for the default form element types.

To install it, run the following command:


yarn add @tailwindcss/forms

# OR

npm install @tailwindcss/forms

Code language: CSS (css)

We installed TailwindCSS into our project during the setup configurations. To publish your tailwind.config.js file, run the following command:


npx tailwindcss init

We are now going to add tailwind forms to the required plugins section of the tailwind.config.js file.


module.exports  =  {

theme:  {

// ...

},

plugins:  [

require('@tailwindcss/forms'),

// ...

],

}

Code language: JavaScript (javascript)

For easy date manipulation and formatting, we will use momentjs. You may install it by running the following command:


yarn add moment

# OR

npm install moment

Code language: PHP (php)

To send the emails, we will an Express.Js. This is a fast, unopinionated, minimalist web framework for Node.js.

To install, run the following command:


yarn add express

# OR

npm install express

Code language: PHP (php)

Nodemailer is a module for Node.Js applications to easily send emails.

Installation is simple, run the following command:


yarn add nodemailer

# OR

npm install nodemailer

Code language: PHP (php)

Vuex is a state management library for Vuex applications.

To create a vuex store, we will create an index.js file in the store folder.


cd store

touch index.js

Code language: CSS (css)

The state is the single source of truth for our applications. It is typically defined as a single object. Here is the state we will define in our store:


// store/index.js

export  const  state  =  ()  =>  ({

names:  {

bride:  "Eve",

groom:  "Adam"

},

date:  {

day:  "SATURDAY",

month:  "AUG",

date:  17,

year:  2022,

time:  "4 PM"

},

address:  {

first:  "Avocado Tree - Volcano of Trust",

second:  "Garden of Eden - Mesopotamia"

}

});

  

Code language: JavaScript (javascript)

Getters allow us to retrieve and compute derived state from store data.

We are going to define the following getters.


// store/index.js

  

export  const  getters  =  {

names:  state  =>  state.names,

date:  state  =>  state.date,

address:  state  =>  state.address

};

Code language: JavaScript (javascript)

Mutations change state in a Vuex store. They are committed in actions. We will define mutations to change the couple’s names, the date, and the address of the wedding:


// store/index.js

  

export  const  mutations  =  {

updateNames(state,  names)  {

state.names  =  names;

return state.names;

},

updateDates(state,  weddingDate,  weddingTime)  {

const  date  =  moment(weddingDate);

state.date.day  =  date.format('dddd').toUpperCase();

state.date.month  =  date.format('MMM').toUpperCase();

state.date.date  =  date.format('D');

state.date.year  =  date.format('YYYY');

state.date.time  =  moment(weddingTime).format('H A');

return  state.date;

},

updateAddress(state,  address)  {

state.address.first  =  address.first;

state.address.second  =  address.second;

return  state.address;

}

};

Code language: JavaScript (javascript)

In the above code snippet, we use date formating to extract the date elements we need. You can read more about date formating with moment.js here.

Actions allow us to commit mutations. We will define one action to commit all our mutations:


// store/index.js

  

export  const  actions  =  {

storeWeddingDetails({  commit  },  formData)  {

commit('updateNames',  formData.names);

commit('updateDates',  formData.date,  formData.time);

return  commit('updateAddress',  formData.address);

}

};

  

Code language: JavaScript (javascript)

With the store above intact, we then create a simple form to dispatch the storeWeddingDetails action with new wedding details. Feel free to check out how we achieve this here: Github – index.vue

We are now going to work towards rendering the templates using Cloudinary.

Download this file and add it to a new nuxtjs-image-wedding-invitation directory in your Cloudinary media library. Ensure it is named v722-aum-35a. Thus the complete public_id should be nuxtjs-image-wedding-invitation/v722-aum-35a.

The main transformation we are going to be using is the text overlay transformation. Here is a code sample:


<!-- components/Invite.vue -->

...

<cld-transformation

:overlay="`text:Sacramento_600_normal:${names.bride},co_rgb:000000`"

gravity="center"

y="-1900"

/>

...

Code language: HTML, XML (xml)

The above transformation ensures the following settings are applied:

  • Overlay is a text overlay

  • Font used is the Sacramento Google Font.

  • Text size is 600px

  • Font weight is normal

  • Content is the bride’s name from the Vue.Js component state

  • Color is black (#000000)

  • Overlay is positioned relative to the center of the image

  • Overlay is shifted left by 1900px (-1900 on the Y-Axis)

We are going to apply similar transformations to our template to fully render the invite. Here is the full file:


<!-- components/Invite.vue -->

<template>

<cld-image

public-id="nuxtjs-image-wedding-invitation/v722-aum-35a"

crop="fill"

alt="Wedding card"

>

<!-- Bride Name -->

<cld-transformation

:overlay="`text:Sacramento_600_normal:${names.bride},co_rgb:000000`"

gravity="center"

y="-1900"

/>

  

<!-- And -->

<cld-transformation

overlay="text:Roboto_100_normal:AND,co_rgb:000000"

gravity="center"

y="-1300"

/>

  

<!-- Husband Name -->

<cld-transformation

:overlay="`text:Sacramento_600_normal:${names.groom},co_rgb:000000`"

gravity="center"

y="-700"

/>

  

<!-- Invitation Text -->

<cld-transformation

overlay="text:Roboto_100_normal:TOGETHER WITH THEIR FAMILIES,co_rgb:000000"

gravity="center"

/>

  

<cld-transformation

overlay="text:Roboto_100_normal:INVITE YOU TO THEIR WEDDING CELEBRATION,co_rgb:000000"

gravity="center"

y="200"

/>

  

<!-- Day -->

<cld-transformation

:overlay="`text:Roboto_100_normal:${date.day},co_rgb:000000`"

y="800"

x="-600"

/>

  

<!-- Month -->

<cld-transformation

:overlay="`text:Roboto_100_normal:${date.month},co_rgb:000000`"

gravity="center"

y="500"

/>

  

<!-- Date -->

<cld-transformation

:overlay="`text:Roboto_100_normal:${date.year},co_rgb:000000`"

gravity="center"

y="1100"

/>

  

<!-- Year -->

<cld-transformation

:overlay="`text:Roboto_400_normal:${date.date},co_rgb:000000`"

gravity="center"

y="800"

/>

  

<!-- Time -->

<cld-transformation

:overlay="`text:Roboto_100_normal:AT ${date.time},co_rgb:000000`"

y="800"

x="500"

/>

  

<!-- Location -->

<cld-transformation

:overlay="`text:Roboto_100_normal:${address.first},co_rgb:000000`"

y="1500"

/>

<cld-transformation

:overlay="`text:Roboto_100_normal:${address.second},co_rgb:000000`"

y="1700"

/>

  

<!-- Reception -->

<cld-transformation

overlay="text:Sacramento_200_normal:reception to follow,co_rgb:000000"

y="2000"

/>

</cld-image>

</template>

  

<script>

import  {  mapGetters  }  from  "vuex";

export  default  {

computed:  {

...mapGetters({

names:  "names",

date:  "date",

address:  "address",

}),

},

};

</script>

  

Code language: HTML, XML (xml)

The above component will get the invite details from our Vuex store using the mapGetters helper. We use it to map store getters to local computed properties.

We will then use the transformations above to render the text onto the invite template.

To send the invites, we will utilize server middlewares in Nuxt.Js. This allows us to define our own middleware which will be run when Nuxt.Js is run.

First, let’s create the middleware file:


mkdir server-middleware

  

cat api.js

Code language: CSS (css)

We are then going to update our nuxt.config.js to recognize and start our middleware.


// nuxt.config.js

  

export  default  {

....

serverMiddleware: [

{

path:  "/api",

handler:  "~/server-middleware/api.js"

},

],

...

}

Code language: JavaScript (javascript)

The above snippet will ensure that any requests to the /api path are routed to the ~/server-middleware/api.js handler.

Let us add some boilerplate code to ensure our api receives requests from the /api/send-email path.


// server-middleware/api.js

  

const  app  =  require('express')()

  

app.all('/send-email',  async  (req,  res)  =>  {

  

res.json({ sent:  true  })

})

  

module.exports  =  app

Code language: JavaScript (javascript)

Our handler will need to access json data in the request body. We are going to use Express.Js’s inbuilt json parser middlware.

Add the following code before the app.all declaration


// server-middleware/api.js

  

const  app  =  require('express')()

  

// Add the next two lines

const  express  =  require('express')

  

app.use(express.json())

  

app.all('/send-email',  async  (req,  res)  =>  {

  

Code language: JavaScript (javascript)

For Nodemailer to send emails, we will need to set up our Gmail credentials. Feel free to create an account if you do not have one. To be able to send emails using Nodemailer, you’ll need to enable less secure access. Feel free to follow this guide. We advise you to create a separate email account as enabling less secure access makes your account more vulnerable to attacks and misuse.

Add MAIL_USERNAME and MAIL_PASSWORD to your .env file


<!-- .env -->

...

MAIL_USERNAME = example@gmail.com

MAIL_PASSWORD = very-secret-strong-password

Code language: HTML, XML (xml)

We will now configure nodemailer to send our emails when the route is called. Here is the entire file compiled together.


// server-middleware/api.js

  

require('dotenv').config()

  

const  app  =  require('express')()

  

const  express  =  require('express')

  

app.use(express.json())

  

const  nodemailer  =  require('nodemailer');

  

app.all('/send-email',  async  (req,  res)  =>  {

  

const  transporter  =  nodemailer.createTransport({

service:  'gmail',

auth:  {

user:  process.env.MAIL_USERNAME,

pass:  process.env.MAIL_PASSWORD,

}

});

  
  

const  body  =  req.body;

  

let  text  =  `Hello ${body.to.name},\n\n`;

text  +=  "We would like to invite you to our wedding ceremony. Please find attached the invitation for more details.\n\n";

text  +=  "Looking forward to seeing you there!\n\n";

text  +=  "Best wishes,\n";

text  +=  `${body.names.bride} and ${body.names.groom}`;

  

var  mailOptions  =  {

from:  process.env.MAIL_USERNAME,

to:  body.to.email,

subject:  `Wedding Invitation from ${body.names.bride} and ${body.names.groom}`,

text,

attachments:  [

{

filename:  `${body.names.bride}-and-${body.names.groom}-wedding-invite.jpg`,

path:  body.invite

},

]

};

  

transporter.sendMail(mailOptions,  function  (error,  info)  {

if  (error)  {

console.log(error);

}  else  {

console.log('Email sent: '  +  info.response);

}

});

  

res.json({ sent:  true  })

})

  

module.exports  =  app

  

Code language: JavaScript (javascript)

The above endpoint does not have any authorization or authentication. For large-scale usage, we advise you to add these and more to ensure your mail service is not misused.

Our middlware is now on standby ready to send emails to our invitees. Let us prepare the data that needs to be sent.

First, we need to get the URL to our invite image. To achieve this, we will get the invite details from our vuex store, render the invite, and then use a recursive method to get the URL.


<!-- pages/invite.vue -->

  

<template>

...

<Invite  id="invite-container" />

...

</template>

  

<script>

import  {  mapGetters  }  from  "vuex";

  

export  default  {

...

computed: {

...mapGetters({

names:  "names",

date:  "date",

address:  "address",

}),

},

mounted()  {

this.getImageURL();

},

methods:  {

getImageURL()  {

const  image  =  document.getElementById("invite-container");

  

if  (image  ===  null)  {

setTimeout(()  =>  {

this.getImageURL();

},  1000);

return;

}

  

if  (image.src)  {

this.image  =  image.src;

}

  

if  (this.image  ==  null)  {

setTimeout(()  =>  {

this.getImageURL();

},  1000);

return;

}

}

...

},

};

</script>

Code language: HTML, XML (xml)

Let us create a simple form to collect invitee details from the user interface. Once the data has been collected, we will send it to the API endpoint and add the invitee to the invitees’ table for UI confirmation.


<!-- pages/invite.vue -->

<template>

<form

action="#"

method="POST"

@submit.prevent="submit"

>

<div>

<label for="name"  >

Name

</label>

<div >

<input

id="name"

name="name"

type="text"

autocomplete="name"

required=""

v-model="form.name"

/>

</div>

</div>

  

<div>

<label for="email"  >

Email address

</label>

<div class="mt-1">

<input

id="email"

name="email"

type="email"

autocomplete="email"

required=""

v-model="form.email"

/>

</div>

</div>

  

<div>

<button

type="submit"

:disabled="image == null"

>

Send Invite

</button>

</div>

</form>

</template>

  

<script>

export  default  {

data()  {

return  {

form:  {

name:  "",

email:  "",

},

invitees:  [],

image:  null,

};

},

...

methods: {

...

async  submit()  {

const submitData  =  {

to:  {

name:  this.form.name,

email:  this.form.email,

},

names:  {

bride:  this.names.bride,

groom:  this.names.groom,

},

invite:  this.image,

};

const response  =  await  fetch("/api/send-email",  {

method:  "POST",

headers:  {

"Content-Type":  "application/json",

},

body:  JSON.stringify(submitData),

});

  

let jsonResponse  =  response.json();

  

console.log(jsonResponse);

  

this.invitees.push({

name:  this.form.name,

email:  this.form.email,

});

},

},

};

</script>

Code language: HTML, XML (xml)

From the above, we have now learned how to prepare and send email invites to our wedding guests in a less stressful manner.

To improve the project, feel free to find a template of your preference, customize the invite contents further, or even link the email sending to a mailing list service for easy guest list management. Let us know if you have any questions or suggestions.

You can find the full code on my Github. https://github.com/musebe/Nuxtjs-wedding-invitation.git

For Further reading, Visit :

Nodemailer Documentation

Cloudinary Transformations

Back to top

Featured Post