Sending push notifications from Kontent.ai

Sending push notifications is a great way to let your customers know about promotions, items they may be interested in, things they left in their shopping cart, and so much more! But, if an editor wants to send a push notification, they’ll need a simple way to accomplish this without needing to know any code.

Eric Dugre

Published on Feb 11, 2020

In this post, we’ll be setting up a content type in Kontent.ai, which editors can use to create new push notifications. When those items are published, your application will automatically send push notifications to all your visitors. What’s more, you can use the content scheduling feature to have the push notifications sent at a specific time in the future!

Setting up your application

In this blog post, we will be referencing the Express JS sample project on GitHub. As it is a Node project, we will install this web push to send the notifications. Generally, push notifications are sent to visitors using JavaScript on the client side, but this library allows us to send push notifications from the server when we receive a webhook from Kontent.ai.

npm i web-push -g

With the web push installed, generate the VAPID keys your application will need to send push notifications. VAPID keys are used to identify your application with a push service and without them, we won’t be able to use the web push. Save these keys in a file somewhere—we’ll use them later in the article.

web-push generate-vapid-keys

As I mentioned above, we’ll be sending push notifications in reaction to a webhook from Kontent.ai. Webhooks are messages that can be sent to your application when something changes, such as a content item being published. The process looks like this:

Kontent.ai


When your application receives the webhook POST, you will need to verify that the request is coming from Kontent.ai using the signature. Install body parser at this time as we need it receive the raw JSON body of the webhook and verify the signature:

npm i body-parser

To ensure that Express uses this parser for JSON data, modify app.js:

const bodyParser = require('body-parser');
//app.use(express.json()); // original code added automatically to Express- comment or remove this line
app.use(bodyParser.raw({type:'application/json'})) // add this line

Allowing visitors to subscribe to notifications

To start off, we need the visitor’s permission to receive push notifications from their browser. This needs to be done on the client side in plain JavaScript to communicate with the browser. In your application’s main layout file, you are most likely rendering a “main” JavaScript file which you can add this logic to. In the Express sample application, we are rendering client.js within /views/layout.pug, so we’ll add our JavaScript to that file:

script(type='text/javascript', src='/scripts/client.js')

The first step to getting permission is to check whether the browser is capable of sending push notifications. It’s important to note that push notifications are only supported over secure SSL connections, so your application needs to be running on HTTPS. The following line will run immediately when the script loads to see if the serviceWorker is available in the browser and will call our own run function if so:

if ('serviceWorker' in navigator) {
    run().catch(error => console.error(error));
}

Now, we define our run function which will register our script with the serviceWorker asynchronously and pass our own callback method:

const run = async() => {
    await navigator.serviceWorker
      .register(`/scripts/worker.js`, {scope: '/scripts/'})
      .then(waitRegistration);
}

Note that we’re registering our own service worker file worker.js in the /public/scripts directory. This is the file that will be called when the push notification is sent by the server. You can create the file now, but leave it blank. We need to make sure the registration is complete before we ask the visitor’s permission. Our waitRegistration function waits until the serviceWorker’s state is “activated” using an event listener, then calls our subscribeForPush function:

const waitRegistration = (reg) => {
    var serviceWorker;
    if (reg.installing) {
        serviceWorker = reg.installing;
    } else if (reg.waiting) {
        serviceWorker = reg.waiting;
    } else if (reg.active) {
        serviceWorker = reg.active;
    }

    if (serviceWorker) {
        if (serviceWorker.state == "activated") {
            subscribeForPush(reg);
        }
        serviceWorker.addEventListener("statechange", function(e) {
            if (e.target.state == "activated") {
                subscribeForPush(reg);
            }
        });
    }
}

Once everything is set up, we can finally ask the visitor for permission. Our subscribeForPush will do that using the subscribe function, and if we have permission, the function will return the PushSubscription object. To subscribe a visitor, we need the public VAPID key we generated at the start of this article. Add the key directly to your JavaScript file:

const publicVapidKey = 'BDRkdCmrfqQ6F-PhAA1AN68jJqHQWARNyxSWFOh1YMpKgju6tHG_dLVxJTgLTXfkXqYFL1VTfcDitw1k4n34IZ8';

Now, we can use it in our subscribeForPush function:

const subscribeForPush = async (reg) => {
    await reg.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array(publicVapidKey)
    })
    .then((sub) => {
       //Store subscription somewhere
    });
}

const urlBase64ToUint8Array = (base64String) => {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');
    const rawData = window.atob(base64);
    return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)));
}

When the visitor subscribes, we receive important information about the subscription which we need to store somewhere:

  • endpoint - a randomized URL pointing to the push service, e.g. https://fcm.googleapis.com/fcm/send/csI_R3I7MN[...]
  • p256dh - subscription key
  • auth - subscription secret

In the Express project, we’re POSTing the data to the /subscribe endpoint and saving it to an SQLite database here, but you can store this information anywhere you’d like (for example, application memory for testing, a text file on your server, etc.).

Configuring the project in Kontent.ai

Now that we’ve built a list of visitors who’d like notifications, we can design the notifications themselves. Our push notifications will be triggered whenever a certain content item is published in Kontent.ai, so go to your project and create a new content type called “Push notification.”

Kontent.ai

This content type will contain the elements required to send the push notification. You can see in the documentation for sending push notifications that we should at least have a title, body, and icon. We can also optionally vibrate the device and open a URL when the notification is clicked, so here are the elements to create in the content type:

  • title: Text
  • body: Text
  • icon: Asset
  • vibrate: Multiple choice (checkbox with single value “Yes”)
  • url: Text

Since we want the push notification to be sent when an item of this type is published, we can go to the Webhooks page and create a new webhook called “Push notifications.” In the Url address field, enter your website’s URL, including the endpoint that will intercept the POST request, e.g., https://mysite.com/push (we will create it in the next section). Also, copy the Secret to be used later. In the Content item events to watch drop-down, select “Publish,” then save the webhook.

Kontent.ai

Validating the webhook

Create an endpoint that will receive the webhook’s POST request in your application. In Express, we register the /push endpoint in app.js:

app.use('/', require('./routes/push'));

Then we create the push route to respond to POST requests:

router.post('/push', (req, res) => {
  //...
}

All webhook notifications have a signature to verify that the request is coming from Kontent.ai. The signature is a hash of the webhook secret and body of the request. You can read more about the signatures here and view an example of verifying the signature in JavaScript here.

To validate the signature in the header of the POST request, you will need to add the webhook secret we copied earlier as a const in your /push endpoint, or use an environment variable. Create a new function to validate the signature:

const crypto = require('crypto');
const webhookSecret = '<your-Kontent-webhook-secret>';
const hasValidSignature = (body, signature) => {
  const computedSignature = crypto.createHmac('sha256', webhookSecret)
      .update(body)
      .digest();
  return crypto.timingSafeEqual(Buffer.from(signature, 'base64'), computedSignature);
}

router.post('/push', (req, res) => {
  //...
}

In the /push handler, call your hasValidSignature function by passing the body and signature of the req.headers object:

app.post('/push', (req, res) => {
  if(hasValidSignature(req.body, req.headers['x-kc-signature'])) {
    res.status(200).send('Success');
  }
  else {
    res.status(403).send('Invalid signature');
  }
});

You can now test your webhook to ensure that you receive a successful 200 response. Publish any content item in Kontent.ai and wait for the POST request to be sent to your application. In Kontent.ai’s Webhooks page, you should see a green dot next to the webhook indicating that it’s successful:

Kontent.ai

Once the webhook is properly validated, we can move on to reading the data that Kontent.ai sent in the body of the webhook notification.

Processing the webhook

We will be using the JavaScript SDK’s Delivery API to retrieve newly-published content item from Kontent.ai. Before you begin this section, follow the steps for installing the Delivery client  and instantiate the client in your /push endpoint:

const { DeliveryClient } = require('@kentico/kontent-delivery');
const deliveryClient = new DeliveryClient({
      projectId: 'your-project-ID',
      globalQueryConfig:  {
        waitForLoadingNewContent: true
      }
});

We want to use the waitForLoadingNewContent option here so that we can get the latest data from Delivery and avoid anything that might be cached.  Let’s take a look at the information given to us in the webhook notification and determine what we need to get the values from our content item. The body of the workflow webhook notification differs from previous non-workflow webhooks. It will look something like this:

"data": {
 "items": [
   {
     "item": {
       "id": "65f05e0f-40c3-436b-a641-e2d4cae16e46"
     },
     "language": {
       "id": "00000000-0000-0000-0000-000000000000"
     },
     "transition_from": {
       "id": "eee6db3b-545a-4785-8e86-e3772c8756f9"
     },
     "transition_to": {
       "id": "03b6ebd3-2f49-4621-92fd-4977b33681d1"
     }
   }
 ]
}

The Item object contains the ID of the content item that was published. Since this webhook triggers only for that workflow step, we don’t need to worry about the transition_from or transition_to values. To send our push notification with the values from the published content item, we will only need to extract the content item’s ID from this response.

To begin this process, create a new function to contain the logic and call it after the signature has been verified:

app.post('/webhook', (req, res) => {
  if(hasValidSignature(req.body, req.headers['x-kc-signature'])) {
    processWebhook(JSON.parse(req.body));
   //...

const processWebhook = (body) => {
}

In our new processWebhook function, we can get the item ID from the JSON body of the response:

const processWebhook = (body) => {
  const updatedVariantItemID = body.data.items[0].item.id;
  //...
}

With the content item ID, we can use the filtering options available in the Delivery API to find the exact content item we’re looking for. Create an Observable from the request, subscribe to it, then unsubscribe when we receive the result:

const sub = deliveryClient.items()
                              .equalsFilter('system.id', updatedVariantContentID)
                              .toObservable()
                              .subscribe(result => {
                                sub.unsubscribe();
                                //...
                              });

Let’s make sure we retrieved the correct content item before continuing. The items function will return an array of items, but there should be only one item in this case. We’ll check the result.items array to make sure there’s at least one item, then only continue if that item is our "push_notification" content type:

if(result.items.length > 0 && result.items[0].system.type == 'push_notification') {
  sendPush(result.items[0]);
 }

If everything looks good, we pass the content item to a new function sendPush which we’ll create in the next section.

Sending the push notification

Now that we have the required information, let’s define our sendPush function to actually send the push notification. You can see an example of what a content item object might look like in our documentation here. We’ll start off by creating the payload for the push notification which is used later by our worker.js file:

const sendPush = function(item) {
  const payload = JSON.stringify({
    title: item.title.value,
    body: item.body.value,
    icon: item.icon.value[0].url,
    vibrate: item.vibrate.value.length > 0,
    url: item.url.value
  });
  //...

To use the web push, first set the VAPID credentials using the public and private keys generated earlier:

const webpush = require('web-push');
webpush.setVapidDetails('mailto:yourmail@yoursite.com', publicVapidKey, privateVapidKey);

Now, we need to call the web push’s sendNotification() function for each visitor that subscribed to the notifications. Get all of your subscribers, then loop through the list. In the Express application, we get all subscriptions from the SQLite database, then construct the required sub object for the web push from the records:

const dao = new AppDAO();
dao.getAllSubscriptions().then((rows) => {
  rows.forEach((row) => {
    let sub = {
          endpoint: row.endpoint,
          keys: {
            p256dh: row.p256dh,
            auth: row.auth
          }
    };
  });
  //Send push...
});

After we construct the subscription object, we can call the web push’s sendNotification and pass the subscription and payload:

webpush.sendNotification(sub, payload).catch(response => {
          if(response.statusCode === 410) {
            //Subscription expired or removed- delete from db
            dao.deleteSubscription(sub);
          }
});

If the request returns a 410 status, the subscription has expired or was manually revoked and should be removed from the database.

Finishing the service worker file

We’re almost done! Remember the worker.js file we created? This is the actual service worker itself, which is responsible for displaying the notification. In the section above, we sent our worker.js file the JSON payload of the push notification, so now this file needs to "catch" the payload and display the notification. Open the file and add the script that will call showNotification:

self.addEventListener('push', ev => {
  const data = ev.data.json();
  var options = {
    body: data.body,
    icon: data.icon,
    data: {url: data.url},
    actions: [{action: "open_url", title: "Read more"}]
  };
  if(data.vibrate) options.vibrate = [200, 100, 200, 100, 200, 100, 200];
  self.registration.showNotification(data.title, options);
});

self.addEventListener('notificationclick', function(event) {
  switch(event.action){
    case 'open_url':
      if(event.notification.data.url !== '') clients.openWindow(event.notification.data.url);
      break;
  }
}
, false);

Here, we’re consuming the payload sent from the web push and displaying the notification. If a URL has been passed, the notification will display a button to open the address, and mobile devices will vibrate if the "Yes" checkbox was checked on the content item.

Now you can test the notification! Access the site and subscribe to notifications. In Kontent.ai, create a new item with the Push notification content type and publish it. When the webhook fires, you will see your notification appear!

Kontent.ai

Push them, but not too often

In this article, we implemented push notifications from the start to the end. We created a content type, configured webhooks, received data in an Express application, and broadcasted them as push notifications. Editors can now easily inform website visitors about the latest news, and it only takes them a few clicks. However, be advised that you should only send these notifications sporadically and within the right context to ensure your visitors are paying attention to them.

You can view the fully-implemented code of the Express application here.

Subscribe to the Kontent.ai newsletter

Get the hottest updates while they’re fresh! For more industry insights, follow our LinkedIn profile.