Automating Emails with Strapi CRON Jobs

Automating Emails with Strapi CRON Jobs

Build a landing page that allows a company to collect leads in the form of user emails and send automated emails at pre-determined intervals.

Author: Alex Godwin

Email automation is a critical component of modern marketing. It allows businesses and enterprises to reach out to a large number of potential customers and to update existing customers on new products and company policies.

Using Strapi cron tasks to automate emails is a powerful concept. In this tutorial, we'll build a landing page that allows a company to collect leads in the form of user emails and send automated emails at pre-determined intervals.

Prerequisites

  • Basic Knowledge of Vue.js,
  • Knowledge of JavaScript, and
  • Node.js (v14 recommended for strapi).

The completed version of your application should look like the image below:

Application Sample Screenshot

Sample 2

An Introduction to Strapi

The Strapi documentation says that "Strapi is a flexible, open-source Headless CMS that gives developers the freedom to choose their favorite tools and frameworks while also allowing editors to manage and distribute their content easily."

Strapi enables the world's largest companies to accelerate content delivery while building beautiful digital experiences by making the admin panel and API extensible through a plugin system.

Scaffolding a Strapi Project

To install Strapi, head over to the Strapi docs at Strapi. We’ll be using the SQLite database for this project. To install Strapi, run the following commands:

    yarn create strapi-app my-project # using yarn
    npx create-strapi-app@latest my-project # using npx

Replace my-project with the name you wish to call your application directory. Your package manager will create a directory with the specified name and install Strapi.

If you have followed the instructions correctly, you should have Strapi installed on your machine. Run the following commands to start the Strapi development server:

    yarn develop # using yarn
    npm run develop # using npm

The development server starts the app on localhost:1337/admin.

Building the Mailing-List Collection Type

Let’s create our Mailing-list collection type which will hold subscribers email addresses:

  1. Click on Content-Type Builder under Plugins on the side menu.
  2. Under collection types, click create new collection type.
  3. Create a new collection-type named Mailing-list .
  4. Create the following fields under *product content-type*:
    • email as Email

Mailing-list collection type

Building the Email-Template Collection Type

Next, we create our Email-template collection type, which will be the model for the Emails we send out:

  1. Click on Content-Type Builder under Plugins on the side menu.
  2. Under collection types, click create new collection type
  3. Create a new collection-type named Email-template .
  4. Create the following fields under product content-type :
    • Subject as Text
    • Content as RichText

Email-template collection type

This will enable us to create the E-mails we send out to our users.

Introduction to CRON Jobs

Cron Jobs are used for a variety of purposes. Cron Jobs are used to schedule tasks on the server to run. They're most typically used to automate system management or maintenance. They are, nevertheless, also relevant in the building of web applications. A web application may be required to conduct specific operations on a regular basis in a variety of circumstances.

Why Use Cron Jobs?

  • If you have a membership site with expiration dates, you can use cron tasks to deactivate or remove accounts that have passed their expiration dates on a regular basis.
  • You have the option of sending out daily newsletter e-mails.
  • Scheduling activities that are carried on a regular basis.

Structure of Cron Jobs

  '* * * * * *'


*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    |
│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)
│    │    │    │    └───── month (1 - 12)
│    │    │    └────────── day of month (1 - 31)
│    │    └─────────────── hour (0 - 23)
│    └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, OPTIONAL)

Setting up Cron Jobs in Strapi

Follow the steps below to set up a cron job in your Strapi application:

  1. Open up the config/server.js file, then add the following lines of code to the server configuration.
    cron: {
        enabled: true,
        tasks: cronTasks,
      }
  1. Create a cron-task.js file in the config folder and add the following
    module.exports = {  
        '01 10 16 * * *': async ({ strapi }) => {
          console.log('cron running')
    }

Creating a cron-job in Strapi is as easy as that.

Setting up an Email Provider in Strapi

We’ll set up the @strapi/provider-email-nodemailer.

  1. To install the package run:

     npm i @strapi/provider-email-nodemailer
    
  2. In config/plugins.js, add the following:

    module.exports = ({ env }) => ({
        email: {
          config: {
            provider: 'nodemailer',
            providerOptions: {
              host: env('SMTP_HOST'),
              port: env('SMTP_PORT'),
              auth: {
                user: env('SMTP_USERNAME'),
                pass: env('SMTP_PASSWORD'),
              },
              pool: true,
              logger: true,
              debug: true,
              maxConnections: 10000
            },

            settings: {
              defaultFrom: env('DEFAULT_EMAIL'),
              defaultReplyTo: env('DEFAULT_EMAIL'),
            },
          },
        },
    });
  1. Create a .env file and add your credentials to it:
SMTP_HOST=YOUR_SMTP_HOST
SMTP_PORT=465
SMTP_USERNAME=YOUR_SMTP_USERNAME
SMTP_PASSWORD=YOUR_SMTP_PASSWORD
DEFAULT_EMAIL=YOUR_EMAIL_ADDRESS

You may use any mail service provider of your choice.

Now that we have our email service, we can update our cron-job.js file to send emails appropriately.

Open up the cron-task.js file and edit it’s content with the following lines of code:

    const marked = require('marked')
    module.exports = {

        '01 13 15 * * *': async ({ strapi }) => {
          console.log('cron running')
          try {
            let emails = await strapi.service('api::email-template.email-template').find()

            emails = emails.results.reverse()

            const subscribers = await strapi.service('api::mailing-list.mailing-list').find()

            const content = marked.parse(emails[0].Content)

            await Promise.all(subscribers.results.map(async (el, i) => {  
              return await strapi
              .plugin('email')
              .service('email')
              .send({
                to: el.email,
                subject: 'Test mail',
                html: content,
              });
            }))

          } catch (error) {
            console.log(error)
          }
        },
      };

You can set the CRON job for what ever time you want. Furthermore, what our logic states is that we’ll get the most recent e-mail from our email-template and send that to all users in our mailing-list.

Building an Application that Collects User Emails and Add Them to Our Mailing List

Obviously, we need a means to collect actual emails for our mailing-list. We’ll build the front-end of our application using Vue 3. Vue is a JavaScript framework for building user interfaces.

Run the following command to install vue:

    mkdir client
    npm init vue@latest

Provide appropriate responses to the prompts, then run the following commands:

    cd <your-project-name>
    npm install
    npm run dev

Your vue app should be running on the specified port. Open up the app.vue file and update it with the following code:

    <script setup>
    import { RouterLink, RouterView } from 'vue-router'
    import HelloWorld from '@/components/HelloWorld.vue'
    </script>
    <template>
      <header>
        <div class="wrapper">
          <HelloWorld msg="Welcome to UoRos" />
          <nav>
            <RouterLink to="/">Home</RouterLink>
            <RouterLink to="/about">About</RouterLink>
          </nav>
        </div>
      </header>
      <RouterView />
    </template>
    <style>
    @import '@/assets/base.css';
    #app {
      max-width: 1280px;
      margin: 0 auto;
      padding: 2rem;
      font-weight: normal;
    }
    header {
      line-height: 1.5;
      max-height: 100vh;
    }
    .logo {
      display: block;
      margin: 0 auto 2rem;
    }
    a,
    .green {
      text-decoration: none;
      color: hsla(160, 100%, 37%, 1);
      transition: 0.4s;
    }
    @media (hover: hover) {
      a:hover {
        background-color: hsla(160, 100%, 37%, 0.2);
      }
    }
    nav {
      width: 100%;
      font-size: 12px;
      text-align: center;
      margin-top: 2rem;
    }
    nav a.router-link-exact-active {
      color: var(--color-text);
    }
    nav a.router-link-exact-active:hover {
      background-color: transparent;
    }
    nav a {
      display: inline-block;
      padding: 0 1rem;
      border-left: 1px solid var(--color-border);
    }
    nav a:first-of-type {
      border: 0;
    }
    @media (min-width: 1024px) {
      body {
        display: flex;
        place-items: center;
      }
      #app {
        display: grid;
        grid-template-columns: 1fr 1fr;
        padding: 0 2rem;
      }
      header {
        display: flex;
        place-items: center;
        padding-right: calc(var(--section-gap) / 2);
      }
      header .wrapper {
        display: flex;
        place-items: flex-start;
        flex-wrap: wrap;
      }
      .logo {
        margin: 0 2rem 0 0;
      }
      nav {
        text-align: left;
        margin-left: -1rem;
        font-size: 1rem;
        padding: 1rem 0;
        margin-top: 1rem;
      }
    }
    </style>

Open up the HelloWorld.vue component and update it with the following code:

    <script setup>
    defineProps({
      msg: {
        type: String,
        required: true
      }
    })
    </script>
    <template>
      <div class="greetings">
        <h1 class="green">{{ msg }}</h1>
        <h3>
          UoRos is a Futuristic company, invested in making sustainabile energy.
        </h3>
        <h3>
          <strong class="bold">
            Solar + Renewable energy
          </strong>
        </h3>
      </div>
    </template>
    <style scoped>
    h1 {
      font-weight: 500;
      font-size: 2.6rem;
      top: -10px;
    }
    h3 {
      font-size: 1.2rem;
    }
    .bold {
      font-weight: 900;
    }
    .greetings h1,
    .greetings h3 {
      text-align: center;
    }
    @media (min-width: 1024px) {
      .greetings h1,
      .greetings h3 {
        text-align: left;
      }
    }
    </style>

In the TheWelcome.vue component, place following code:

    <script setup>
    import WelcomeItem from './WelcomeItem.vue'
    import DocumentationIcon from './icons/IconDocumentation.vue'
    import ToolingIcon from './icons/IconTooling.vue'
    import EcosystemIcon from './icons/IconEcosystem.vue'
    import CommunityIcon from './icons/IconCommunity.vue'
    import SupportIcon from './icons/IconSupport.vue'
    </script>
    <template>
      <WelcomeItem>
        <template #icon>
          <DocumentationIcon />
        </template>
        <template #heading>Vision</template>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Voluptatibus corrupti eveniet asperiores assumenda, sit quia eligendi porro exercitationem! Vitae ab veniam dolorum voluptates! Iste rerum molestiae nobis tenetur unde odit.
      </WelcomeItem>
      <WelcomeItem>
        <template #icon>
          <ToolingIcon />
        </template>
        <template #heading>Purpose</template>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Alias optio, facere velit officiis dolore doloremque ipsa minima, explicabo, fugiat placeat debitis repellat. Assumenda accusantium enim, aspernatur nemo illo ducimus quia.
        <br />
      </WelcomeItem>
      <WelcomeItem>
        <template #icon>
          <EcosystemIcon />
        </template>
        <template #heading>Ecosystem</template>
        Lorem, ipsum dolor sit amet consectetur adipisicing elit. Architecto beatae officia culpa animi quas labore! Ducimus tempora, voluptatibus illo laudantium laborum repudiandae tempore labore fugit at excepturi dolore placeat minus?
      </WelcomeItem>
    </template>

Update the About.vue component with the following code:

    <template>
      <div class="about">
        <div>
          <h1 class="heading">
            About UoRos
          </h1>
          <p class="desc_text">
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse maiores velit molestias assumenda numquam eligendi. Perferendis pariatur, eligendi at nostrum odio ducimus quod consequatur dignissimos culpa magni commodi, quo esse!
          </p>
          <h1 class="heading">
            What we do
          </h1>
          <p class="desc_text">
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Esse maiores velit molestias assumenda numquam eligendi. Perferendis pariatur, eligendi at nostrum odio ducimus quod consequatur dignissimos culpa magni commodi, quo esse!
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Placeat voluptatibus officia atque fuga nesciunt rem iste ab saepe ducimus, praesentium soluta quis temporibus, tempora voluptas facere quos reprehenderit beatae in!
          </p>
          <h1 class="heading">
            Join our mailing list
          </h1>
          <p class="desc_text">
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Delectus eum assumenda dolor laudantium sed temporibus nulla sunt dicta voluptatibus asperiores harum officiis, at, quibusdam beatae corrupti. Suscipit placeat modi corrupti!
          </p>
          <div>
            <form action="" @submit="signUpToMail">
              <p>{{ error }}</p>
              <input type="email" class="email_input" name="email" placeholder="Email address" id="" v-model="email">
              <input type="submit" class="email_submit" value="Sign up">
            </form>
          </div>
        </div>
      </div>
    </template>
    <style>
    @media (min-width: 1024px) {
      .about {
        min-height: 100vh;
        display: flex;
        align-items: center;
      }
      .heading {
        /* margin: 10px 0; */
        font-weight: 700;
      }
      .desc_text {
        margin: 0 0 15px 0;
      }
      .email_input {
        padding: 12px 10px;
        font-size: 16px;
        border: none;
        background: rgb(232, 240, 254)
      }
      .email_submit {
        background-color: hsla(160, 100%, 37%, 1); /* Green */
        border: none;
        color: white;
        padding: 12px 10px;
        text-align: center;
        text-decoration: none;
        display: inline-block;
        font-size: 16px;
        margin: 0 5px;
      }
    }
    </style>
    <script>
    import axios from 'axios'
    export default {
      data() {
        return {
          email: '',
          error: ''
        }
      },
      methods: {
        async signUpToMail(e) {
          e.preventDefault()
          if(!this.email) return this.error = `Enter an email address`
          const data = {
            data: {email: this.email}
          }
          console.log('Email', this.email)
          try {
            await axios(`http://localhost:1337/api/mailing-lists`, {
              method: 'POST',
              data
            })

          } catch (error) {
            console.log(error) 
          }

          this.email = ''
          this.error = ''
        }
      }

    }
    </script>

This component is responsible for the API call to our Strapi server.

The old Strapi documentation says this: Please note that Strapi's built in CRON feature will not work if you plan to use pm2 or node based clustering. You will need to execute these CRON tasks outside of Strapi.

The Problem

You could for example deploy your Strapi application in multiple containers/clusters/instances, to boost the performance (horizontal scaling). This could for example be in multiple pm2 clusters or Google App engine instances, depending on the hosting.

If you have deployed your Strapi application in multiple instances, then each of those will trigger the cron job and then for example you could have duplicate emails sending because it was triggered & executed in more then one instance.

A solution for this could be to use an third party service that handles the scheduling with for example cron and then uses an custom endpoint to trigger (in one instance) the custom code once.

Possible Solution

You could create a custom route/controller that would perform the same logic as a crontask (maybe implement API tokens to secure it). Then you would call this endpoint from any other system.

Could be the built in CRON job system in Linux with a simple curl request or using a more complex self hosted cron system. You can even use things like Zapier or IFTTT, doesn't matter what it is so long as it can make the GET, POST, or PUT request you want (usually GET).

This way the "crontask" endpoint will only be executed on one node of the cluster.

Conclusion

Finally we have our full application, which would enable us collect emails and send them new information about our company regularly at a scheduled time.