Bryan Soltis

@bryan_soltis

Creating a Progressive Web App with a Headless CMS — Part 2

April 2nd 2018

Part 1 | Part 2 | Part 3

Hey, welcome back! In Part 1 of this series, I introduced you to the Progressive Web App (PWA). I covered what they are, how they function, and how a headless CMS can really add some awesome functionality. In Part 2, I’m going to dive into building a PWA and adding some cool features. That means creating the base app, adding the magic, and testing.

So, let’s get to the good stuff!

Recap

In Part 1, I mentioned a few services that we’re going to talk about a lot in this series.

Google PWA Tutorial
This is the “base” app we started with for our demo. It’s a great tutorial and provides a nice foundation for a PWA.

Kentico Cloud
This is the cloud-based CMS we used to power our application. It’s a scalable, flexible CMS with tons of SDKs and sample projects to help you get going quickly.

Creating the base app

OK, let’s get to creating your app. You can start from scratch, or use one of the many demos available to jumpstart your development. Regardless of the path you take, your app will contain a few key components.

Application Shell (App Shell)
 The Application Shell is the base UI for the app. This is what you will load initially to give the user a consistent experience, regardless of their connection and device. This component is often stored in the browser cache the first time the app is loaded, as it will not change very often. You should also include any controls, buttons, or images in the App Shell to help those get loaded quickly, too.

Service Worker
 Possibly the most important component, your Service Worker is what will power you PWA with fresh content. When people use a PWA, they will be expecting a rich, responsive experience. Service Workers are how you make that happen. These components are responsible for handling asynchronous events and executing code to update the UI. You have full control over the life-cycle of the script and state management.

Manifest
 To have an app-like experience, your PWA needs to tell the browser how to behave. In your manifest file, you’ll define the basics of your application, along with the URL and icons to load. You can even set the orientation, full screen mode, and other environment variables to ensure your content loads exactly how you want.

The Real Code
 After you define your AppShell, service worker, and manifest, you need to add the functionality. What I mean by this is the code that fetches your data, updates your layout, and any other features you want to include. It may be a standalone JaveScript file, or a bundle of them. I’m also grouping any external libraries into this mix, in case you are planning on adding some sweet cursor trails to your design.

Making the App Shell

For our demo, we started with a basic layout of a header, title, and a list of POIs. By using a standard top bar, the PWA would have a familiar and consistent look in the browser and on mobile devices. This basically made up our App Shell, so we kept it as simple as possible.

This is the extent of my design skills.

Also included in the App Shell is the basic layout for our POIs. For this, we use a template to display the elements. Not very exciting, but that’s what 1st versions of an app are for!

<main class=”main”>
<div class=”card cardTemplate” hidden>
<h2 class=”title”></h2>
<div class=”content”></div>
<a class=”map-link” target=”_blank” hidden>Open the map</a>
</div>
</main>

We will use this template when we write out POIs to the app after the content is fetched.

Adding a Service Worker

The next piece to add is the Service Worker. file. You will want to set up your triggers, define your cached information, and configure how the app will function. This can have big impact on how the app behaves, and what makes it more like a native app to mobile users. You and your Service Worker are probably going to spend a lot of time together, so bring it a house warming gift as an ice breaker.

In our demo, we created a new script to hold the service worker logic. Building off the sample, we added our cache information, list of files we wanted to store, and our event listeners. With this worker, we can capture events as the user interacts with the app, and control how the functionality processes based on the request and/or environment.

var dataCacheName = 'packAndGoData-v1';
var cacheName = 'packAndGoApp-v1';
var filesToCache = [
'/',
'/index.html',
'/main.js',
'/manifest.json',
'/styles/fonts/RobotoMedium.eot',
'/styles/fonts/RobotoMedium.svg',
'/styles/fonts/RobotoMedium.ttf',
'/styles/fonts/RobotoMedium.woff',
'/styles/fonts/RobotoMedium.woff2',
'/styles/style.css',
'/assets/images/icon_refresh.svg'|
];
self.addEventListener('install', function (e) {
console.log('[ServiceWorker] Install');
e.waitUntil(
caches.open(cacheName).then(function (cache) {
console.log('[ServiceWorker] Caching app shell');
return cache.addAll(filesToCache);
})
);
});
self.addEventListener('activate', function (e) {
console.log('[ServiceWorker] Activate');
e.waitUntil(
caches.keys().then(function (keyList) {
return Promise.all(keyList.map(function (key) {
if (key !== cacheName && key !== dataCacheName) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);

/*
* Fixes a corner case in which the app wasn't returning the latest data.|
* You can reproduce the corner case by commenting out the line below and
* then doing the following steps: 1) load app for first time so that the
* initial New York City data is shown 2) press the refresh button on the
* app 3) go offline 4) reload the app. You expect to see the newer NYC
* data, but you actually see the initial data. This happens because the
* service worker is not yet activated. The code below essentially lets
* you activate the service worker faster.
*/
return self.clients.claim();
});
self.addEventListener('fetch', function (e) {
console.log('[Service Worker] Fetch', e.request.url);
var dataUrl = 'https://deliver.kenticocloud.com/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/items';
if (e.request.url.indexOf(dataUrl) > -1) {
/*
* When the request URL contains dataUrl, the app is asking for fresh
* weather data. In this case, the service worker always goes to the
* network and then caches the response. This is called the "Cache then
* network" strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-then-network
*/
e.respondWith(
caches.open(dataCacheName).then(function (cache) {
return fetch(e.request).then(function (response) {
cache.put(e.request.url, response.clone());
return response;
})
})
);
} else {
/*
* The app is asking for app shell files. In this scenario the app uses the
* "Cache, falling back to the network" offline strategy:
* https://jakearchibald.com/2014/offline-cookbook/#cache-falling-back-to-network
*/
e.respondWith(
caches.match(e.request).then(function (response) {
return response || fetch(e.request)
})
);
}
});

Integrating Kentico Cloud

With the foundation for the app set, you are ready to add the good stuff. If you are going the cloud-based CMS route, this is where you will want to leverage any APIs and SDKs they have available. These will help you quickly integrate the platform into your PWA, ensuring seamless communication between the systems. Chances are, they have several SDK flavors available, so pick one that matches your architecture.

For our PWA, we selected a JavaScript SDK for Kentico Cloud. Because our app would be JavaSscript-centric, this SDK offered the best solution to quickly incorporate into our architecture. By executing a few npm commands, we had our SDK installed within our app and ready for programming. This resulted in some new package files for creating our CMS client and retrieving content.

You can learn more about the Kentico Cloud JavaScript SDK.

Adding the content

OK, so you’ve just spent all this time learning the architecture, defining your content, and creating your functionality. Now it’s time to make it work! This is where your “ Real Code” comes into play. You will need to connect your app to your CMS, fetch some content, and wire up your service worker to handle the requests.

For our app, we created a few scripts to hold our functionality. First, we created a client.js file to define our CMS client. This file leveraged the JavaScript SDK we integrated into the site, making the client creation a breeze. Because we used Kentico Cloud for the app, this meant defining our project id and creating a new DeliveryClient for accessing our content.

import { DeliveryClient, DeliveryClientConfig } from 'kentico-cloud-delivery-typescript-sdk';
const projectId = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXX;
const previewApiKey = "";
const isPreview = () => {
return previewApiKey !== "";
}
const client = new DeliveryClient(
new DeliveryClientConfig(projectId, [],
{
enablePreviewMode: isPreview(),
previewApiKey: previewApiKey
}
)
)
module.exports = {
projectId,
client
}

Next, we added an app.js file for the main functionality. Following the Google PWA Tutorial, we coded how the POI cards would be updated and when.

Because PWAs leverage cache heavily, we added functionality to check if the POI data had already been retrieved. If so, the app would load that content. Otherwise, we used our Kentico Cloud Delivery client to retrieve the information from our project.

const getPointsOfInterest = () => {
const url = 'https://deliver.kenticocloud.com/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX/items?system.type=point_of_interest';
if ('caches' in window) {
/*
* Check if the service worker has already cached this data about the Point of interests
* data. If the service worker has the data, then display the cached
* data while the app fetches the latest data.
*/
caches.match(url).then(response =>
response && response
.json()
.then(json => {
const typedResponse = new BaseResponse(json, response);
responseMapService
.mapMultipleResponse(typedResponse, client.config)
.items.forEach(pointOfInterest =>
updatePointOfInterestCard(pointOfInterest))
})
);
}
client.items()
.type('point_of_interest')
.get()
.toPromise()
.then(response =>
response.items.forEach(pointOfInterest => {
updatePointOfInterestCard(pointOfInterest);
}))
}

In both cases, we call the updatePointOfInterestCard function to update our layout with the content.

Next, we created our updatePointOfInterestCard function to interact with our page elements. We check to make sure the card exists, creating it if needed. Then, we find each element and update the value with the retrieved data. Because we are using an SDK, we leverage some pre-built functions and patterns to help set our content easily.

const updatePointOfInterestCard = (data) => {
const key = data.system.id;
const title = data.title.value;
const content = data.description.value
const latitude = data.latitude__decimal_degrees_ && data.latitude__decimal_degrees_.value;
const longitude = data.longitude__decimal_degrees_ && data.longitude__decimal_degrees_.value;
let card = visibleCards[key];
if (!card) {
card = cardTemplate.cloneNode(true);
card.classList.remove('cardTemplate');
card.removeAttribute('hidden');
container.appendChild(card);
visibleCards[key] = card;
}
card.querySelector('.title').textContent = title;
card.querySelector('.content').innerHTML = content;
if (latitude && longitude) {
card.querySelector('.map-link').setAttribute('href',
`http://maps.google.com/?ie=UTF8&hq=&ll=${latitude},${longitude}&z=16`)
card.querySelector('.map-link').removeAttribute('hidden');
}
if (isLoading) {
loader.setAttribute('hidden', true);
isLoading = false;
}
};

Configuring the manifest

OK, you have your awesome app ready to go. You have your layout, service worker, and functionality all integrated, but you need to get it looking good on all devices. This is where your manifest file comes into play. In this file, you’ll define the experience the user will see, what options will be available to them, and how the app will look on their device.

In our PWA, we set some basics in our manifest for the title and colors. For the display, we selected standalone. This causes the page look like a native app, removing the address bar. Lastly, we defined our icons. Because PWAs can be pinned to user’s home screen, it’s important to include all the sizes that may be used.

{
"name": "Travel app",
"short_name": "travelapp",
"theme_color": "#1564bf",
"background_color": "#e1e2e1",
"display": "standalone",
"Scope": "/",
"start_url": "/",
"icons": [
{
"src": "\/assets\/images\/icons\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/assets\/images\/icons\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/assets\/images\/icons\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/assets\/images\/icons\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/assets\/images\/icons\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/assets\/images\/icons\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
],
"splash_pages": null
}

Testing the app

At this point, you should be able to test your app. You will want to view it on your computer and your mobile device. You should see a consistent, clean look across the environments. On your phone, it should look very similar to a native app, and enable you to interact with the home screen and any other functionality you added.

For our demo, I opened the app on my computer and checked the layout.

I then used the Developer Tools to simulate a mobile device. This was to test that the responsive layout properly resized the content.

Next, I deployed the app to an Azure App Service to access the site externally. I opened the app on a mobile device to confirm it displayed properly.

Tip
I had to run the npm run build-prod command to create the dist folder for the deployment.

Because of the manifest.json settings, I was able to add the app to the home screen and confirm the icon displayed correctly.

If you want to see the live demo, check out my site. I’ll continue to update this deployment with new features as the blog series continues.

Next time

Whew, that was a lot of information to type! If you’ve been following along, you’re no doubt as tired as I am and due for a break. In this blog, I covered setting up my base application, adding my service workers and layouts, and caching files and data at various points. After ensuring all my code was in place, I fired up the app and tested it on a variety of devices.

In future blogs, I will be covering more advanced topics like push notifications, images, and native app features. I’ll keep building on the demo mentioned here, so I hope you enjoy the journey. Until next time, friends!

If you want to keep track of our PWA demo app, check out our Pack and Go GitHub project.

Part 1 | Part 2 | Part 3

More by Bryan Soltis

More Related Stories