Progressive Mailchimp sign-up form with Netlify

This article describes solution for JAMstack website, that usually requires custom back-end. I’ll give more details and code to build a solid “works-at-all-times” sign-up form with a custom serverless function

Background

I’m currently running a curated newsletter for amazon sellers. I’ve been looking for a similar service - found non. So I started it, mostly to build audience I was interested in serving.

Almost one year in… with a very basic service mash-up of Carrd and Mailchimp we managed to grow with average 70 subscribers a month (with not much marketing and zero budget) and some sponsorship content.

I’ve been thinking of growing this newsletter beyond email to increase our reach, so to say.. I wrote down some requirements for version 2 of this project and decided to build a JAMstack website.

There have been a lot of nice to have’s, but main requirements have been the following:

I will focus specifically on contact form and subscription form in this article. For a lot of people, this seemed like non-trivial functionality to get working with a static website.

Netlify

I was already using Netlify for a couple of projects and experience was great. So I found no reason to look for anything else, but you might want to explore other options – JAMstack services are booming. Most of ideas I describe here are easily transferable to other platforms.

In Netlify case, it offers a lot of useful building blocks for a static website. With time, I managed to find some rough edges and edges cases that don’t work. But for a lot of projects Netlify offers a complete solution of nicely integrated and easy to use functionality. And you are always free to pick any other third party that suits you better or even roll with your own.

Contact form was really easy to implement with Netlify Forms. It took me less than 5 minutes to have contact form with basic spam filters up an running. It was amazing, but will not work for a mailchimp subscription form in a way I’d wanted it. I can hookup serverless functions to forms, but there is no sane way to validate input and make sure to have mailchimp keys checked.

So, here is where Netlify functions come in play..

Netlify Functions

To make a short analogy to explain - everything you heard about AWS Lambda is true for Netlify Functions. But list of supported languages is shorter, only Go and JavaScript. I’d love it if they had Ruby support, but Javascript is good enough too.

I’ve looked around to find any suitable ready-made code, but found examples that didn’t fit my initial requirements. Which are:

So, I set out to build my own function and will drive you through implementation I ended up adopting.

Implementation

If you’re here for code - check out my public gist or read further for comments

1. Implementing a form submission

This is a very basic HTML form. But there are to important parts to note:

  1. Endpoint (/.netlify/functions/form-handler) I used for form submission - is a function that we will create shortly
  2. div element with id “message” is our placeholder for success messages
<div id="message"></div>
<form id="newsletter" class="subscribe" action="/.netlify/functions/form-handler" method="post">
  <input type="email" id="inputEmail" name='email' placeholder="Enter email to subscribe for FREE" class="email" required autofocus>
  <button class="button" type="submit">Subscribe</button>
</form>

2. Implementing an AJAX request

This part is really horrible – I’m not using TypeScript or jQuery, it’s just standard JavaScript and it works everywhere.

Again, two important parts to notice:

<script>
  var form = document.getElementById('newsletter');

  form.addEventListener("submit", function(e) {
    e.preventDefault();
    email = document.getElementById('inputEmail').value;
    submitEmail(email)
  });

  function submitEmail(email) {
    fetch('/.netlify/functions/form-handler', {
        method: 'post',
        body: JSON.stringify({
          email: email
        })
      }).then(function(response) {
        return response.json();
      }).then(function(data) {
        messageDiv = document.getElementById('message');
        messageDiv.innerText = 'Confirmation email has been sent!'
      });
  };
</script>

3. Setting up environment for functions

I ended-up directly using only 3 JavaScript libraries (and shit ton of transitional dependencies that go with them) to make this function work:

I’ve added these lines to packages.json file (including some convenience methods to get netlify-lambda working locally)

{
  "scripts": {
    "start": "netlify-lambda serve source/lambda",
    "build": "netlify-lambda build source/lambda"
  },
  "dependencies": {
    "axios": "^0.19.0",
    "dotenv": "^8.1.0",
    "netlify-lambda": "^1.6.3"
  }
}

4. Building a netlify function

There are some important elements here to notice:

Other then that, this seems like a straight forward method that passes some data to 3-rd party API.

import { parse } from 'querystring'
const axios = require('axios');
const mailChimpAPI = process.env.MAILCHIMP_API_KEY;
const mailChimpListID = process.env.MAILCHIMP_LIST_ID;


exports.handler = (event, context, callback) => {
  let body = {}
  console.log(event)
  try {
    body = JSON.parse(event.body)
  } catch (e) {
    body = parse(event.body)
  }

  if (!body.email) {
    console.log('missing email')
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        error: 'missing email'
      })
    })
  }

  if (!mailChimpAPI) {
    console.log('missing mailChimpAPI key')
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        error: 'missing mailChimpAPI key'
      })
    })
  }

  if (!mailChimpListID) {
    console.log('missing mailChimpListID key')
    return callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        error: 'missing mailChimpListID key'
      })
    })
  }

  const data = {
    email_address: body.email,
    status: "pending",
    merge_fields: {}
  };

  const subscriber = JSON.stringify(data);
  console.log("Sending data to mailchimp", subscriber);

  // Subscribe an email

  axios(
    {
      method: 'post',
      url: `https://us19.api.mailchimp.com/3.0/lists/${mailChimpListID}/members/`, //change region (us19) based on last values of ListId.
      data: subscriber,
      auth: {
        username: 'apikey', // any value will work 
        password: mailChimpAPI
      }
    }
  ).then(function(response){
    console.log(`status:${response.status}` )
    console.log(`data:${response.data}` )
    console.log(`headers:${response.headers}` )

    if (response.headers['content-type'] === 'application/x-www-form-urlencoded') {
      // Do redirect for non JS enabled browsers
      return callback(null, {
        statusCode: 302,
        headers: {
          Location: '/thanks.html',
          'Cache-Control': 'no-cache',
        },
        body: JSON.stringify({})
      });
    }

    // Return data to AJAX request
    return callback(null, {
      statusCode: 200,
      body: JSON.stringify({ emailAdded: true })
    })
  }).catch(function(error) {
    if (error.response) {
      // The request was made and the server responded with a status code
      // that falls out of the range of 2xx
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // The request was made but no response was received
      console.log(error.request);
    } else {
      // Something happened in setting up the request that triggered an Error
      console.log('Error', error.message);
    }
    console.log(error.config);
  });
};

Conclusions

This functionality is live - so it’s a good indication that I’m quite satisfied and confident with solution. It’s an amazing experience to get so far with a static website. It just works (really fast!), it’s cheap and it’s easy to scale.

This PageSpeed Insight result has been completely unintended benefit of JAMstack.

Alt Text

But there are issues in this code I didn’t anticipated. Later I hope to address these:

Thanks for reading!

Did anyone built side-projects with JAMstack sites? How was your experience?

*****
A dream will always triumph over reality, once it is given a chance.(c)