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-invitation23# OR45npx create-nuxt-app nuxtjs-email-wedding-invitation67# OR89npm 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-invitation234yarn dev56#OR78npm 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/cloudinary23# OR45npm install @nuxtjs/cloudinary
We will then add @nuxtjs/cloudinary
as a module in the modules section of nuxt.config.js
:
1// nuxt.config.js2345export default {67...89modules:[1011'@nuxtjs/cloudinary'1213]1415}
To configure the module, we will create a cloudinary
section in nuxt.config.js
.
1// nuxt.config.js23export default{45...67cloudinary: {89cloudName: process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME,1011secure: true,1213useComponent: true1415}1617}
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 -->23NUXT_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/forms23# OR45npm 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 = {23theme: {45// ...67},89plugins: [1011require('@tailwindcss/forms'),1213// ...1415],1617}
MomentJs
For easy date manipulation and formatting, we will use momentjs. You may install it by running the following command:
1yarn add moment23# OR45npm 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 express23# OR45npm install express
Nodemailer
Nodemailer is a module for Node.Js applications to easily send emails.
Installation is simple, run the following command:
1yarn add nodemailer23# OR45npm 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 store23touch 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.js23export const state = () => ({45names: {67bride: "Eve",89groom: "Adam"1011},1213date: {1415day: "SATURDAY",1617month: "AUG",1819date: 17,2021year: 2022,2223time: "4 PM"2425},2627address: {2829first: "Avocado Tree - Volcano of Trust",3031second: "Garden of Eden - Mesopotamia"3233}3435});
Getters
Getters allow us to retrieve and compute derived state from store data.
We are going to define the following getters.
1// store/index.js2345export const getters = {67names: state => state.names,89date: state => state.date,1011address: state => state.address1213};
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.js2345export const mutations = {67updateNames(state, names) {89state.names = names;1011return state.names;1213},1415updateDates(state, weddingDate, weddingTime) {1617const date = moment(weddingDate);1819state.date.day = date.format('dddd').toUpperCase();2021state.date.month = date.format('MMM').toUpperCase();2223state.date.date = date.format('D');2425state.date.year = date.format('YYYY');2627state.date.time = moment(weddingTime).format('H A');2829return state.date;3031},3233updateAddress(state, address) {3435state.address.first = address.first;3637state.address.second = address.second;3839return state.address;4041}4243};
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.js2345export const actions = {67storeWeddingDetails({ commit }, formData) {89commit('updateNames', formData.names);1011commit('updateDates', formData.date, formData.time);1213return commit('updateAddress', formData.address);1415}1617};
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 -->23...45<cld-transformation67:overlay="`text:Sacramento_600_normal:${names.bride},co_rgb:000000`"89gravity="center"1011y="-1900"1213/>1415...
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 -->23<template>45<cld-image67public-id="nuxtjs-image-wedding-invitation/v722-aum-35a"89crop="fill"1011alt="Wedding card"1213>1415<!-- Bride Name -->1617<cld-transformation1819:overlay="`text:Sacramento_600_normal:${names.bride},co_rgb:000000`"2021gravity="center"2223y="-1900"2425/>26272829<!-- And -->3031<cld-transformation3233overlay="text:Roboto_100_normal:AND,co_rgb:000000"3435gravity="center"3637y="-1300"3839/>40414243<!-- Husband Name -->4445<cld-transformation4647:overlay="`text:Sacramento_600_normal:${names.groom},co_rgb:000000`"4849gravity="center"5051y="-700"5253/>54555657<!-- Invitation Text -->5859<cld-transformation6061overlay="text:Roboto_100_normal:TOGETHER WITH THEIR FAMILIES,co_rgb:000000"6263gravity="center"6465/>66676869<cld-transformation7071overlay="text:Roboto_100_normal:INVITE YOU TO THEIR WEDDING CELEBRATION,co_rgb:000000"7273gravity="center"7475y="200"7677/>78798081<!-- Day -->8283<cld-transformation8485:overlay="`text:Roboto_100_normal:${date.day},co_rgb:000000`"8687y="800"8889x="-600"9091/>92939495<!-- Month -->9697<cld-transformation9899:overlay="`text:Roboto_100_normal:${date.month},co_rgb:000000`"100101gravity="center"102103y="500"104105/>106107108109<!-- Date -->110111<cld-transformation112113:overlay="`text:Roboto_100_normal:${date.year},co_rgb:000000`"114115gravity="center"116117y="1100"118119/>120121122123<!-- Year -->124125<cld-transformation126127:overlay="`text:Roboto_400_normal:${date.date},co_rgb:000000`"128129gravity="center"130131y="800"132133/>134135136137<!-- Time -->138139<cld-transformation140141:overlay="`text:Roboto_100_normal:AT ${date.time},co_rgb:000000`"142143y="800"144145x="500"146147/>148149150151<!-- Location -->152153<cld-transformation154155:overlay="`text:Roboto_100_normal:${address.first},co_rgb:000000`"156157y="1500"158159/>160161<cld-transformation162163:overlay="`text:Roboto_100_normal:${address.second},co_rgb:000000`"164165y="1700"166167/>168169170171<!-- Reception -->172173<cld-transformation174175overlay="text:Sacramento_200_normal:reception to follow,co_rgb:000000"176177y="2000"178179/>180181</cld-image>182183</template>184185186187<script>188189import { mapGetters } from "vuex";190191export default {192193computed: {194195...mapGetters({196197names: "names",198199date: "date",200201address: "address",202203}),204205},206207};208209</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-middleware2345cat api.js
We are then going to update our nuxt.config.js
to recognize and start our middleware.
1// nuxt.config.js2345export default {67....89serverMiddleware: [1011{1213path: "/api",1415handler: "~/server-middleware/api.js"1617},1819],2021...2223}
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.js2345const app = require('express')()6789app.all('/send-email', async (req, res) => {10111213res.json({ sent: true })1415})16171819module.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.js2345const app = require('express')()6789// Add the next two lines1011const express = require('express')12131415app.use(express.json())16171819app.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 -->23...45MAIL_USERNAME = example@gmail.com67MAIL_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.js2345require('dotenv').config()6789const app = require('express')()10111213const express = require('express')14151617app.use(express.json())18192021const nodemailer = require('nodemailer');22232425app.all('/send-email', async (req, res) => {26272829const transporter = nodemailer.createTransport({3031service: 'gmail',3233auth: {3435user: process.env.MAIL_USERNAME,3637pass: process.env.MAIL_PASSWORD,3839}4041});4243444546const body = req.body;47484950let text = `Hello ${body.to.name},\n\n`;5152text += "We would like to invite you to our wedding ceremony. Please find attached the invitation for more details.\n\n";5354text += "Looking forward to seeing you there!\n\n";5556text += "Best wishes,\n";5758text += `${body.names.bride} and ${body.names.groom}`;59606162var mailOptions = {6364from: process.env.MAIL_USERNAME,6566to: body.to.email,6768subject: `Wedding Invitation from ${body.names.bride} and ${body.names.groom}`,6970text,7172attachments: [7374{7576filename: `${body.names.bride}-and-${body.names.groom}-wedding-invite.jpg`,7778path: body.invite7980},8182]8384};85868788transporter.sendMail(mailOptions, function (error, info) {8990if (error) {9192console.log(error);9394} else {9596console.log('Email sent: ' + info.response);9798}99100});101102103104res.json({ sent: true })105106})107108109110module.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 -->2345<template>67...89<Invite id="invite-container" />1011...1213</template>14151617<script>1819import { mapGetters } from "vuex";20212223export default {2425...2627computed: {2829...mapGetters({3031names: "names",3233date: "date",3435address: "address",3637}),3839},4041mounted() {4243this.getImageURL();4445},4647methods: {4849getImageURL() {5051const image = document.getElementById("invite-container");52535455if (image === null) {5657setTimeout(() => {5859this.getImageURL();6061}, 1000);6263return;6465}66676869if (image.src) {7071this.image = image.src;7273}74757677if (this.image == null) {7879setTimeout(() => {8081this.getImageURL();8283}, 1000);8485return;8687}8889}9091...9293},9495};9697</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 -->23<template>45<form67action="#"89method="POST"1011@submit.prevent="submit"1213>1415<div>1617<label for="name" >1819Name2021</label>2223<div >2425<input2627id="name"2829name="name"3031type="text"3233autocomplete="name"3435required=""3637v-model="form.name"3839/>4041</div>4243</div>44454647<div>4849<label for="email" >5051Email address5253</label>5455<div class="mt-1">5657<input5859id="email"6061name="email"6263type="email"6465autocomplete="email"6667required=""6869v-model="form.email"7071/>7273</div>7475</div>76777879<div>8081<button8283type="submit"8485:disabled="image == null"8687>8889Send Invite9091</button>9293</div>9495</form>9697</template>9899100101<script>102103export default {104105data() {106107return {108109form: {110111name: "",112113email: "",114115},116117invitees: [],118119image: null,120121};122123},124125...126127methods: {128129...130131async submit() {132133const submitData = {134135to: {136137name: this.form.name,138139email: this.form.email,140141},142143names: {144145bride: this.names.bride,146147groom: this.names.groom,148149},150151invite: this.image,152153};154155const response = await fetch("/api/send-email", {156157method: "POST",158159headers: {160161"Content-Type": "application/json",162163},164165body: JSON.stringify(submitData),166167});168169170171let jsonResponse = response.json();172173174175console.log(jsonResponse);176177178179this.invitees.push({180181name: this.form.name,182183email: this.form.email,184185});186187},188189},190191};192193</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 :