DEV Community

Cover image for How to make a serverless webhook for email alerts, using Gatsby Functions
Kurt Lekanger
Kurt Lekanger

Posted on

How to make a serverless webhook for email alerts, using Gatsby Functions

Using Gatsby Functions, it's easy to make a serverless function that automatically sends out email alerts triggered by an event - in our case when new content is published in Contentful (a headless CMS).

In a series of articles, I have shown how I created a new website for the condominium where I live using React/Gatsby, Chakra UI and Auth0. Read part 1 of the series here: How I built our condos's new web pages with Gatsby and Chakra UI

The main reason why we wanted a website in the first place, was the need for a place to publish new or important information to our residents. However, I quickly realized that it was cumbersome to manually send email to notify residents every time we had published new information in our headless CMS system.

Using Gatsby Functions it's quite easy to make an API that sends an email alert to subscribers. In this article I'll walk you through how I have set up a web hook where Contentful sends a POST request to my email alert API. The API checks if the request is allowed by verifying a secret key, and then fetches a list of all registered users and sends out an email alert to those users that have chosen to receive alerts.

For email I chose to use Sendgrid. I also considered Nodemailer, but Sendgrid has a nice email template feature where my API can fetch URL, article title and excerpt from Contentful, and pass this information along to the email template in Sendgrid. Then the user receiving the email can click on a link in the email to open the article that was just published on the condo's web site.

To make everything work, I had to set up the following:

  • Create a webhook in Contentful
  • Make a serverless function (our API) with Gatsby Functions
  • Create an email template in Sendgrid

What I have made could easily be adapted to other needs or purposes. If you're not using Gatsby, you don't need to make any big changes to make it work with Netlify Functions or Next.js API routes. The code is pretty similar to how you would create this using the Node.js Express framework.

Setting up a webhook in Contentful

All content on the condominium's website is published in the headless CMS solution Contentful. Under the Settings menu in Contentful, you have an option called Webhooks. Here I created a webhook that will be triggered only when new content is published, and then send a POST request to the email API I intend to create.

After clicking Add Webhook and giving the webhook a name, I had to specify that it should be a POST request that will go to the URL of my email API.

screenshot, set up webhook in Contentful.

Under Content Events I specify that the webhook should be called only when new content has been published.

screenshot, configure triggers for webhook in Contentful.

I don't want everyone to be able to make requests to my API and spam my users with email. So I created a secret header which is a key/value pair that is sent in the header of the API request. A secret key the API will verify before allowing email to be sent.

I called my secret key CONTENTFUL_WEBHOOK_SECRET and used a password generator to make a long password with numbers, letters and special characters.

screenshot, add secret header in Contentful.

Create the email API using Gatsby Functions

Gatsby Functions is an Express-like way of building APIs for your Gatsby projects. It's serverless functions, which of course does not mean that you don't need servers, but you don't have to think about the servers. Just focus on writing code - just the way I like it.

To make a new Gatsby Function you create a JavaScript or TypeScript file in the folder /src/api and export a handler function which takes two parameters - req (request) and res (response).

I created a file called email-alert-users.ts in the api/admin-users folder (use .js if you do not use TypeScript). This means that my API endpoint will be https://url-to-my-site-admin/admin-users/email-alert-users.

In the first part of the code, I first check that the HTTP method is POST. Then I check that the header contains CONTENTFUL_WEBHOOK_SECRET and that it matches the secret key I have stored as an environment variable (locally in a .env file, and at Netlify under Deploy Settings and Environment. Make sure you do not submit this to Github - check that you have .env* in your .gitignore file.

Here is the first part of the code, with the necessary checks:

// src/api/admin-users/email-alert-users.ts

import { GatsbyFunctionRequest, GatsbyFunctionResponse } from 'gatsby';
const ManagementClient = require('auth0').ManagementClient;
const sgMail = require('@sendgrid/mail');

export default async function handler(
  req: GatsbyFunctionRequest,
  res: GatsbyFunctionResponse
) {
  if (req.method !== `POST`) {
    return res.status(405).json({
      error: 'method not allowed',
      error_description: 'You should do a POST request to access this',
    });
  }

  // Check if secret key received from Contentful web hook matches the one in the .env file
  if (
    req.headers.contentful_webhook_secret !==
    process.env.CONTENTFUL_WEBHOOK_SECRET
  ) {
    return res.status(401).json({
      error: 'unauthorized',
      error_description: 'The Contentful web hook secret key is not correct',
    });
  }

// ...code continues
Enter fullscreen mode Exit fullscreen mode

If you're not using TypeScript, remove GatsbyFunctionRequest and GatsbyFunctionResponse, this is just for type checking.

At the top I import ManagementClient to be able to connect to Auth0's Management API, so that I can fetch a list of all users who should receive email. You can of course get this user list from other places as well, such as a database.

Set up the email contents

The emails should contain the following, in addition to a standard text stating that there is new content (I set this up at Sendgrid):

  • Title and introduction
  • URL of the article

Some of the content on the condominiums website is only available to logged in users, using client-only routes in my Gatsby project, more specifically under the route /information. All open articles are under /blog. This means that I must first check if it is a private article or not, and then build the correct URL based on this. When the Contentful webhook makes a POST request to my API, it passes on lots of info about what is published in the body of the request. I have created a field in Contentful called privatePost that can be set to true or false (this is a checkbox you tick when you write an article in the Contentful editor). This makes it easy to create the correct URL by letting the API check this field first.

I also retrieve the article title and excerpt (short intro about the article) in this way:

// src/api/admin-users/email-alert-users.ts

  let articleURL;
  if (req.body.fields.privatePost.nb === true) {
    articleURL = `https://gartnerihagen-askim.no/informasjon/post/${req.body.fields.slug.nb}/${req.body.sys.id}`;
  } else {
    articleURL = `https://gartnerihagen-askim.no/blog/${req.body.fields.slug}/${req.body.sys.id}`;
  }

  const articleTitle = req.body.fields.title.nb;
  const articleExcerpt = req.body.fields.excerpt.nb;

// ...code continues
Enter fullscreen mode Exit fullscreen mode

Get all email subscribers

Now that the email content has been retrieved, I connect to Auth0's Management API and request the permission ("scope") read:users. Then I use the getUsers() method to fetch all the users. Then I use a combination of .filter and .map to create a list of only users who have subscribed to email. I have stored this information in the user_metadata field for each user in Auth0.

I now have a list of all users who want email. I use sgMail.setApiKey(process.env.SENDGRID_API_KEY) to send an API key to access my Sendgrid account. Then I set up the email content and store it in the msg constant. msg contains an array of email recipients, the sender's email address, and a templateId. The latter is the ID of an email template I have set up at Sendgrid. Under dynamic_template_data I can pass along information about the article URL, title and excerpt of the article, which will then be captured by Sendgrid and filled out in the email template.

Finally, the email is sent with sgMail.sendMultiple (msg).

// src/api/admin-users/email-alert-users.ts

// Connect to the Auth0 management API
  const auth0 = new ManagementClient({
    domain: `${process.env.GATSBY_AUTH0_DOMAIN}`,
    clientId: `${process.env.AUTH0_BACKEND_CLIENT_ID}`,
    clientSecret: `${process.env.AUTH0_BACKEND_CLIENT_SECRET}`,
    scope: 'read:users',
  });

  try {
    const users = await auth0.getUsers();

    // Filter out only those users that have subscribed to email alerts
    // This is defined in the user_metadata field on Auth0
    const userEmails = users
      .filter((user) => {
        return (
          user &&
          user.user_metadata &&
          user.user_metadata.subscribeToEmails === true
        );
      })
      .map((user) => user.email);

    // using Twilio SendGrid's v3 Node.js Library
    // https://github.com/sendgrid/sendgrid-nodejs
    sgMail.setApiKey(process.env.SENDGRID_API_KEY);

    const msg = {
      to: userEmails,
      from: 'Boligsameiet Gartnerihagen <post@gartnerihagen-askim.no>',
      templateId: 'd-123456789',  // The ID of the dynamic Sendgrid template
      dynamic_template_data: {
        articleURL,
        articleTitle,
        articleExcerpt,
      },
    };

    await sgMail.sendMultiple(msg);

// ...code continues
Enter fullscreen mode Exit fullscreen mode

If everything has worked as expected, I return status 200 and some info. I do everything inside a try/catch block, and in case of an error I return an error code.

res.status(200).json({
      body: {
        status_code: 200,
        status_description: 'Emails are sent to all subscribed users',
        userEmails,
      },
    });
  } catch (error) {
    res.status(error.statusCode).json({
      error: error.name,
      message: error.message,
      status_code: error.statusCode || 500,
      error_description: error.message,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Configuring the Sendgrid email template

I could of course "hard code" the email contents, but with "dynamic Templates" in Sendgrid you can customize the email with the information you want. In my case information about the article that was published, including an URL.

To set up a dynamic email template in Sendgrid, go to sendgrid.com, log in and select Email APIDynamic Templates from the settings menu. Then tap Create a Dynamic Template, then Add Version. You can now choose to use Sendgrid's design editor to design your email, or you can use an HTML editor. I used the HTML editor and added some standard text.

To insert dynamic content in the text - such as title or URL- you can use variable names. In my email API, I use the variable name articleTitle for the article title I receive from my CMS. To use this variable in the Sendgrid template, just insert {{articleTitle}} into the HTML code.

On the right hand side (see screenshot below) you can see what the email will look like. If you press Test Data in the menu at the top, you can enter test data for the various variables you use in the email template to check that everything looks OK.

Screenshot, html editor in Sendgrid

That's it! We now have created an email alert service that sends out an email every time new content is published in our headless CMS.

I also updated the user admin dashboard for our web pages, so that admins can turn on or off email alerts for users. The users are also able to do this by themself on their "My page" when logged in:

Screenshot, mypage on the condominiums web page

The source code for this project is on my Github. If you're just interested in the email alert Gatsby Function, you'll find it here.

In this article you can read about how I made a user admin dashboard with Gatsby Functions and Auth0

This is a translation, original article is published on my personal web site lekanger.no

Top comments (0)