Wedding invitation app in NuxtJS and Nodemailer

Eugene Musebe

Introduction

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

Codesandbox

The final project demo can be viewed on Codesandbox.

Setting things up

Nuxt project setup

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:

1yarn create nuxt-app nuxtjs-email-wedding-invitation
2
3# OR
4
5npx create-nuxt-app nuxtjs-email-wedding-invitation
6
7# OR
8
9npm init nuxt-app nuxtjs-email-wedding-invitation

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:

1cd nuxtjs-email-wedding-invitation
2
3
4yarn dev
5
6#OR
7
8npm run dev

Cloudinary setup

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:

1yarn add @nuxtjs/cloudinary
2
3# OR
4
5npm install @nuxtjs/cloudinary

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

1// nuxt.config.js
2
3
4
5export default {
6
7...
8
9modules:[
10
11'@nuxtjs/cloudinary'
12
13]
14
15}

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

1// nuxt.config.js
2
3export default{
4
5...
6
7cloudinary: {
8
9cloudName: process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME,
10
11secure: true,
12
13useComponent: true
14
15}
16
17}

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

1cat .env
1<!-- .env -->
2
3NUXT_ENV_CLOUDINARY_CLOUD_NAME = secret-cloud-name

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.

Tailwind forms

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:

1yarn add @tailwindcss/forms
2
3# OR
4
5npm install @tailwindcss/forms

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

1npx tailwindcss init

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

1module.exports = {
2
3theme: {
4
5// ...
6
7},
8
9plugins: [
10
11require('@tailwindcss/forms'),
12
13// ...
14
15],
16
17}

MomentJs

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

1yarn add moment
2
3# OR
4
5npm install moment

Express

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:

1yarn add express
2
3# OR
4
5npm install express

Nodemailer

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

Installation is simple, run the following command:

1yarn add nodemailer
2
3# OR
4
5npm install nodemailer

Generating the invites

Vuex store

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.

1cd store
2
3touch index.js

State

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:

1// store/index.js
2
3export const state = () => ({
4
5names: {
6
7bride: "Eve",
8
9groom: "Adam"
10
11},
12
13date: {
14
15day: "SATURDAY",
16
17month: "AUG",
18
19date: 17,
20
21year: 2022,
22
23time: "4 PM"
24
25},
26
27address: {
28
29first: "Avocado Tree - Volcano of Trust",
30
31second: "Garden of Eden - Mesopotamia"
32
33}
34
35});

Getters

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

We are going to define the following getters.

1// store/index.js
2
3
4
5export const getters = {
6
7names: state => state.names,
8
9date: state => state.date,
10
11address: state => state.address
12
13};

Mutations

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:

1// store/index.js
2
3
4
5export const mutations = {
6
7updateNames(state, names) {
8
9state.names = names;
10
11return state.names;
12
13},
14
15updateDates(state, weddingDate, weddingTime) {
16
17const date = moment(weddingDate);
18
19state.date.day = date.format('dddd').toUpperCase();
20
21state.date.month = date.format('MMM').toUpperCase();
22
23state.date.date = date.format('D');
24
25state.date.year = date.format('YYYY');
26
27state.date.time = moment(weddingTime).format('H A');
28
29return state.date;
30
31},
32
33updateAddress(state, address) {
34
35state.address.first = address.first;
36
37state.address.second = address.second;
38
39return state.address;
40
41}
42
43};

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

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

1// store/index.js
2
3
4
5export const actions = {
6
7storeWeddingDetails({ commit }, formData) {
8
9commit('updateNames', formData.names);
10
11commit('updateDates', formData.date, formData.time);
12
13return commit('updateAddress', formData.address);
14
15}
16
17};

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

Rendering the invite

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

Template setup

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.

Transformations

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

1<!-- components/Invite.vue -->
2
3...
4
5<cld-transformation
6
7:overlay="`text:Sacramento_600_normal:${names.bride},co_rgb:000000`"
8
9gravity="center"
10
11y="-1900"
12
13/>
14
15...

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:

1<!-- components/Invite.vue -->
2
3<template>
4
5<cld-image
6
7public-id="nuxtjs-image-wedding-invitation/v722-aum-35a"
8
9crop="fill"
10
11alt="Wedding card"
12
13>
14
15<!-- Bride Name -->
16
17<cld-transformation
18
19:overlay="`text:Sacramento_600_normal:${names.bride},co_rgb:000000`"
20
21gravity="center"
22
23y="-1900"
24
25/>
26
27
28
29<!-- And -->
30
31<cld-transformation
32
33overlay="text:Roboto_100_normal:AND,co_rgb:000000"
34
35gravity="center"
36
37y="-1300"
38
39/>
40
41
42
43<!-- Husband Name -->
44
45<cld-transformation
46
47:overlay="`text:Sacramento_600_normal:${names.groom},co_rgb:000000`"
48
49gravity="center"
50
51y="-700"
52
53/>
54
55
56
57<!-- Invitation Text -->
58
59<cld-transformation
60
61overlay="text:Roboto_100_normal:TOGETHER WITH THEIR FAMILIES,co_rgb:000000"
62
63gravity="center"
64
65/>
66
67
68
69<cld-transformation
70
71overlay="text:Roboto_100_normal:INVITE YOU TO THEIR WEDDING CELEBRATION,co_rgb:000000"
72
73gravity="center"
74
75y="200"
76
77/>
78
79
80
81<!-- Day -->
82
83<cld-transformation
84
85:overlay="`text:Roboto_100_normal:${date.day},co_rgb:000000`"
86
87y="800"
88
89x="-600"
90
91/>
92
93
94
95<!-- Month -->
96
97<cld-transformation
98
99:overlay="`text:Roboto_100_normal:${date.month},co_rgb:000000`"
100
101gravity="center"
102
103y="500"
104
105/>
106
107
108
109<!-- Date -->
110
111<cld-transformation
112
113:overlay="`text:Roboto_100_normal:${date.year},co_rgb:000000`"
114
115gravity="center"
116
117y="1100"
118
119/>
120
121
122
123<!-- Year -->
124
125<cld-transformation
126
127:overlay="`text:Roboto_400_normal:${date.date},co_rgb:000000`"
128
129gravity="center"
130
131y="800"
132
133/>
134
135
136
137<!-- Time -->
138
139<cld-transformation
140
141:overlay="`text:Roboto_100_normal:AT ${date.time},co_rgb:000000`"
142
143y="800"
144
145x="500"
146
147/>
148
149
150
151<!-- Location -->
152
153<cld-transformation
154
155:overlay="`text:Roboto_100_normal:${address.first},co_rgb:000000`"
156
157y="1500"
158
159/>
160
161<cld-transformation
162
163:overlay="`text:Roboto_100_normal:${address.second},co_rgb:000000`"
164
165y="1700"
166
167/>
168
169
170
171<!-- Reception -->
172
173<cld-transformation
174
175overlay="text:Sacramento_200_normal:reception to follow,co_rgb:000000"
176
177y="2000"
178
179/>
180
181</cld-image>
182
183</template>
184
185
186
187<script>
188
189import { mapGetters } from "vuex";
190
191export default {
192
193computed: {
194
195...mapGetters({
196
197names: "names",
198
199date: "date",
200
201address: "address",
202
203}),
204
205},
206
207};
208
209</script>

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.

Sending the invites

Server middleware configuration

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:

1mkdir server-middleware
2
3
4
5cat api.js

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

1// nuxt.config.js
2
3
4
5export default {
6
7....
8
9serverMiddleware: [
10
11{
12
13path: "/api",
14
15handler: "~/server-middleware/api.js"
16
17},
18
19],
20
21...
22
23}

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

Sending the email

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

1// server-middleware/api.js
2
3
4
5const app = require('express')()
6
7
8
9app.all('/send-email', async (req, res) => {
10
11
12
13res.json({ sent: true })
14
15})
16
17
18
19module.exports = app

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

1// server-middleware/api.js
2
3
4
5const app = require('express')()
6
7
8
9// Add the next two lines
10
11const express = require('express')
12
13
14
15app.use(express.json())
16
17
18
19app.all('/send-email', async (req, res) => {

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

1<!-- .env -->
2
3...
4
5MAIL_USERNAME = example@gmail.com
6
7MAIL_PASSWORD = very-secret-strong-password

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

1// server-middleware/api.js
2
3
4
5require('dotenv').config()
6
7
8
9const app = require('express')()
10
11
12
13const express = require('express')
14
15
16
17app.use(express.json())
18
19
20
21const nodemailer = require('nodemailer');
22
23
24
25app.all('/send-email', async (req, res) => {
26
27
28
29const transporter = nodemailer.createTransport({
30
31service: 'gmail',
32
33auth: {
34
35user: process.env.MAIL_USERNAME,
36
37pass: process.env.MAIL_PASSWORD,
38
39}
40
41});
42
43
44
45
46const body = req.body;
47
48
49
50let text = `Hello ${body.to.name},\n\n`;
51
52text += "We would like to invite you to our wedding ceremony. Please find attached the invitation for more details.\n\n";
53
54text += "Looking forward to seeing you there!\n\n";
55
56text += "Best wishes,\n";
57
58text += `${body.names.bride} and ${body.names.groom}`;
59
60
61
62var mailOptions = {
63
64from: process.env.MAIL_USERNAME,
65
66to: body.to.email,
67
68subject: `Wedding Invitation from ${body.names.bride} and ${body.names.groom}`,
69
70text,
71
72attachments: [
73
74{
75
76filename: `${body.names.bride}-and-${body.names.groom}-wedding-invite.jpg`,
77
78path: body.invite
79
80},
81
82]
83
84};
85
86
87
88transporter.sendMail(mailOptions, function (error, info) {
89
90if (error) {
91
92console.log(error);
93
94} else {
95
96console.log('Email sent: ' + info.response);
97
98}
99
100});
101
102
103
104res.json({ sent: true })
105
106})
107
108
109
110module.exports = app

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.

Sending email data to our API

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.

1<!-- pages/invite.vue -->
2
3
4
5<template>
6
7...
8
9<Invite id="invite-container" />
10
11...
12
13</template>
14
15
16
17<script>
18
19import { mapGetters } from "vuex";
20
21
22
23export default {
24
25...
26
27computed: {
28
29...mapGetters({
30
31names: "names",
32
33date: "date",
34
35address: "address",
36
37}),
38
39},
40
41mounted() {
42
43this.getImageURL();
44
45},
46
47methods: {
48
49getImageURL() {
50
51const image = document.getElementById("invite-container");
52
53
54
55if (image === null) {
56
57setTimeout(() => {
58
59this.getImageURL();
60
61}, 1000);
62
63return;
64
65}
66
67
68
69if (image.src) {
70
71this.image = image.src;
72
73}
74
75
76
77if (this.image == null) {
78
79setTimeout(() => {
80
81this.getImageURL();
82
83}, 1000);
84
85return;
86
87}
88
89}
90
91...
92
93},
94
95};
96
97</script>

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.

1<!-- pages/invite.vue -->
2
3<template>
4
5<form
6
7action="#"
8
9method="POST"
10
11@submit.prevent="submit"
12
13>
14
15<div>
16
17<label for="name" >
18
19Name
20
21</label>
22
23<div >
24
25<input
26
27id="name"
28
29name="name"
30
31type="text"
32
33autocomplete="name"
34
35required=""
36
37v-model="form.name"
38
39/>
40
41</div>
42
43</div>
44
45
46
47<div>
48
49<label for="email" >
50
51Email address
52
53</label>
54
55<div class="mt-1">
56
57<input
58
59id="email"
60
61name="email"
62
63type="email"
64
65autocomplete="email"
66
67required=""
68
69v-model="form.email"
70
71/>
72
73</div>
74
75</div>
76
77
78
79<div>
80
81<button
82
83type="submit"
84
85:disabled="image == null"
86
87>
88
89Send Invite
90
91</button>
92
93</div>
94
95</form>
96
97</template>
98
99
100
101<script>
102
103export default {
104
105data() {
106
107return {
108
109form: {
110
111name: "",
112
113email: "",
114
115},
116
117invitees: [],
118
119image: null,
120
121};
122
123},
124
125...
126
127methods: {
128
129...
130
131async submit() {
132
133const submitData = {
134
135to: {
136
137name: this.form.name,
138
139email: this.form.email,
140
141},
142
143names: {
144
145bride: this.names.bride,
146
147groom: this.names.groom,
148
149},
150
151invite: this.image,
152
153};
154
155const response = await fetch("/api/send-email", {
156
157method: "POST",
158
159headers: {
160
161"Content-Type": "application/json",
162
163},
164
165body: JSON.stringify(submitData),
166
167});
168
169
170
171let jsonResponse = response.json();
172
173
174
175console.log(jsonResponse);
176
177
178
179this.invitees.push({
180
181name: this.form.name,
182
183email: this.form.email,
184
185});
186
187},
188
189},
190
191};
192
193</script>

Conclusion

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

Eugene Musebe

Software Developer

I’m a full-stack software developer, content creator, and tech community builder based in Nairobi, Kenya. I am addicted to learning new technologies and loves working with like-minded people.