Progressive web applications (PWAs) continue to gain widespread attention, acceptance, and compatibility with web browsers due to their native-like attributes. One of the mandatory security considerations to deploying these applications is that they must be hosted securely. Due to this, PWA features will not work on a non-secure URL, i.e. a URL that does not use the secure https://
protocol. In this post, we will create an automated deployment pipeline that deploys our PWA to a secure URL on Firebase.
Prerequisites
To follow this post, a few things are required:
- Basic knowledge of Javascript
- Node.js installed on your system
- An HTTP Server Module installed globally on your system (
npm install -g http-server
) - A Firebase account
- A CircleCI account
- A GitHub account
With all these installed and set up, let’s begin the tutorial.
Setting up the demo application
The first task is to create our demo application that we will be deploying to Firebase. Run the following commands to create a directory for the project and go into the root of the directory:
mkdir my-pwa-firebase
cd my-pwa-firebase
Next, let’s create our application home page. Create a file named index.html
at the root of the project and paste the following code into it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="manifest" href="manifest.json" />
<meta name="theme-color" content="#db4938" />
<link rel="stylesheet" type="text/css" href="styles.css" media="all" />
<title>DogVille</title>
</head>
<body>
<h2>
Welcome to the home of Dogs
</h2>
<div class="dog-list">
<div class="dog-pic"><img width="300px" src="images/dog1.jpg" /></div>
<div class="dog-pic"><img width="300px" src="images/dog2.jpg" /></div>
<div class="dog-pic"><img width="300px" src="images/dog3.jpg" /></div>
<div class="dog-pic"><img width="300px" src="images/dog4.jpg" /></div>
</div>
<script src="app.js"></script>
</body>
</html>
In the home page file above, we link to three files: manifest.json
which we will use to set up the Add to Home Screen PWA feature, styles.css
to apply some basic styling to our page, and app.js
which will load in the service worker that we haven’t created yet, but will shortly. In the body of our page, we have a title that reads Welcome to the home of Dogs and below it, we display a list of dog pictures. As you can see, this is a dog site (apologies to the cat lovers).
The dog images are contained in an images
folder at the root of the project. I have referenced the dog pictures in index.html
according to the filenames used in images
. Go ahead and create this folder, you can download dog pictures for free here and rename them appropriately.
Let’s add styles by creating the file styles.css
at the root of our project and pasting the following code in it:
body {
background-color: orange;
}
h2 {
color: white;
}
This file gives our page background color and makes the title text white. Let’s take our app for a spin by running the following command at the root of the project to invoke the global http-server
module to spin up a local server to serve our app:
http-server
You will see a screen similar to the one below when you load the URL in your browser (your dogs will likely be different from mine):
Adding a service worker
Now, let’s add the juice of PWAs, the service worker. Create a serviceworker.js
file at the root of your project and paste the following code in it:
var cacheName = "sw-v1";
var filesToCache = [
"./",
"./index.html",
"./styles.css",
"./app.js",
"./images/dog1.jpg",
"./images/dog2.jpg",
"./images/dog3.jpg",
"./images/dog4.jpg"
];
self.addEventListener("install", function (e) {
console.log("<ServiceWorker> ---- Install v1");
e.waitUntil(
caches.open(cacheName).then(function (cache) {
console.log("<ServiceWorker> --- Caching app shell");
return cache.addAll(filesToCache);
})
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(self.clients.claim());
});
self.addEventListener("fetch", function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
});
In our service worker file above, we cache all our static files including our project root and our images. We then listen for the install
event to install our service worker and create a cache for these files using the specified cacheName
as the identifier.
Next, we listen to the activate
event to ensure that any new service worker that has been installed is the one serving our cache and not an old version.
Finally, we listen to the fetch
event to intercept any requests and check whether we already have the requested resource in our cache. If so, we serve the cached version, and if not we make a new request to fetch the resource.
Now, let’s load our service worker file into our application. Create an app.js
file at the root of the project and paste the following code in it:
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker.register("./serviceworker.js").then(
function (registration) {
console.log("Hurray! Service workers with scope: ", registration.scope);
},
function (err) {
console.log("Oops! ServiceWorker registration failed: ", err);
}
);
});
}
Let’s take our service worker for a spin. Make sure that your app is still running, then do a hard reload on the browser tab where the application is currently loaded (Ctrl + Shift + R
). Now check the browser console to see the console log messages we wrote to confirm the installation of the service worker. You will see the following messages in your console.
To confirm that we now have offline capabilities with our service worker installed, shut down the http-server
service with Ctrl + C
, then refresh the application on the browser. You would usually see the offline page at this point because the application is no longer running, but with the magic that is a service worker, you can still see your application home page. Also notice that all dog pictures are loading offline.
Great!
Adding a manifest file
Let’s wrap up our demo application by creating our manifest.json
file at the root of the project. You can generate a simple manifest.json
file with icons here.
Below is the code in my manifest.json
file.
Note: I have removed some icons in my example.
{
"name": "my-dogsite-pwa",
"short_name": "my-dogsite-pwa",
"theme_color": "#000000",
"background_color": "#ffffff",
"display": "standalone",
"orientation": "portrait",
"scope": "/index.html",
"start_url": "/index.html",
"icons": [
{
"src": "icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
In the above file, the name
of the application, the short name
, the preferred orientation
, the theme_color
for the app bar, and the background_color
for the splash screen are defined.
Note: It is a best practice to add the short_name
, an optional field that specifies the name that will be displayed in the app launcher or a new tab page. Otherwise, the name
will be used, and it will be truncated if it is more than 12 characters long.
The icons
array is also added with a collection of icon object definitions that point to the icons we will be using for our PWA. Remember to create the icons
folder and fill it with the icons named in the manifest file.
Setting up deployment to Firebase
Now that our PWA is complete, let’s begin preparing it for deployment to Firebase. You need to have Firebase tools installed. To check that you have it installed, run the following command:
firebase
This will return a list of Firebase commands to your CLI. If it doesn’t, you need to run the following command to get it installed:
npm install -g firebase-tools
You also need to run the above command if your firebase-tools
is less than version 8. To check your firebase-tools
version, run the following command:
firebase --version
To set up Firebase hosting for our project, we need to create a Firebase project. Head over to your Firebase console and create a new Firebase project.
Click Add Project
and enter the name of your project in the first page that pops up.
Click Continue
and on the next page about adding Google Analytics, turn off the Enable Google Analytics for this project
toggle button. Since this is a demo project, we won’t be needing analytics.
Now click Create Project
. Wait for Firebase to complete setting up your project then click Continue
to navigate to your project dashboard.
Now that we have our project set up, the next step is to set up our PWA to be hosted on Firebase using the project we just created. Stay logged into Firebase on your default browser, then go to your CLI to run the following command:
firebase login:ci
This command will log you into Firebase by redirecting to your browser where you’re currently logged in. Once the authentication process is complete, your Firebase token will be printed on the screen just below the line that reads ✔ Success! Use this token to login on a CI server
. Save this token securely because you will need it later on in this tutorial.
Next, run the following command at the root of your PWA project to initialize the Firebase setup:
firebase init
The first prompt you will get from this command is
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices.
. Navigate to Hosting
option using the arrow keys, hit Spacebar
to select, and hit Enter
to go to the next prompt.
The next prompts is to associate your local project with a Firebase project on your Firebase account. From here, you can choose to use an existing project or create a new one. Select Use an existing project
and hit Enter
to move to the next prompt. This selection will prompt the CLI tool to load your Firebase projects for you to select from in the next prompt. I am selecting the project we just created on our Firebase console.
Hit Enter
to confirm your selection.
The next prompt asks for the project folder and suggests the folder public
. For our project, everything is taking place at the root, so simply type /
and hit Enter
to proceed.
The next prompt is ? Configure as a single-page app (rewrite all urls to /index.html)?
. As our entire application resides within index.html
, type y
and hit Enter
. This prompt is important for distinguishing between single-page apps and traditional multi-page apps so that Firebase Hosting knows how to handle them.
The next prompt detects that we already have an index.html
file and asks if it should be overwritten. Type N
for this and hit Enter
. This completes the setup and you will now have a .firebaserc
file which sets the project id for this application, a firebase.json
file which contains details about the options we selected during the set up process and some other default settings, and a standard .gitignore
file for Firebase.
With this, we can now proceed to creating our deployment pipeline.
Building the CD pipeline
To set up our automated deployment pipeline, we need to take the following steps:
- Push our project to a remote repository (GitHub in this case) connected to our CircleCI account
- Add our application as a new project on CircleCI
- Add our Firebase token as an environment variable to our CircleCI project
- Install
firebase-tools
locally in the project - Create our pipeline confirguration file
- Push project changes to our repository to initiate deployment
Let’s begin. Scaffold a quick package.json
file by running the following command:
npm init -y
Then, push the project to GitHub.
The next step is to set up the repository for our project as a CircleCI project.
On the CircleCI console, go to the Add Projects page.
Click Set Up Project. This will load the next screen.
On the setup page, click Start Building. Before the build starts, you get a prompt to either download and use the provided CircleCI configuration file and have it on a separate branch or to set one up manually.
Select Add Manually to proceed. This will prompt another dialog that checks to confirm that you have a configuration file set up to begin building.
Click on Start Building to complete the setup. This will immediately trigger the pipeline. The build will fail because we haven’t added our pipeline configuration file.
Our next step is to add our Firebase token as an environment variable in the CircleCI project we just created. On the Pipelines page, with our project selected, click Project Settings.
On the settings page side-menu, click Environment Variables
. On the variables set up page, click Add Environment Variable
. A dialog box will appear. In the Name*
field, enter FIREBASE_TOKEN
, and in the Value*
field, paste in the Firebase token you got from your CLI in the step above. Click Submit to complete the process. You now have the token variable registered.
Return to the PWA project on your system. Run the following command to install firebase-tools
at the root of the project so you can have it registered in package.json
as a development dependency:
npm install -D firebase-tools
Once that process is complete, its time to create our deployment configuration file. At the root of your project, create a folder named .circleci
, and in it a file named config.yml
. Inside the config.yml
file, enter the following code:
version: 2
jobs:
build:
docker:
- image: circleci/node:10.16.0
working_directory: ~/repo
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
- v1-dependencies-
- run:
name: Install Dependencies
command: npm install
- save_cache:
key: v1-npm-deps-{{ checksum "package-lock.json" }}
paths:
- ./node_modules
- run:
name: Deploy to Firebase
command: ./node_modules/.bin/firebase deploy --token "$FIREBASE_TOKEN" --only hosting
In the configuration file above, we start by checking out the project from our remote repository. We then install our dependencies, cache them, and run our firebase-tools
from the local installation to use our Firebase token to deploy our application.
Now the moment of truth. Let’s commit our changes and push them to our repository to cause our deployment script to be triggered and deploy our application to Firebase hosting.
Click into the build to see the behind-the-scenes of the project.
From the Deploy to Firebase
section. You can see the URL of the deployed application. For this exercise, it’s https://my-dog-site-pwa.web.app
. Load yours into your browser to test your application.
As you can see above, the address bar is loading our Firebase URL, and we can see console messages indicating that our service worker is installed. If you turn off your network and refresh this page, you will see that the application, with all the dog pictures and styling, loads instead of the usual offline screen.
Conclusion
Setting up SSL certificates for https://
URLs is not a task most developers enjoy. Sometimes this leads to reluctance in adopting PWAs. However in this post, we have demonstrated how to set up an automated continuous integration / continuous deployment pipeline for secured hosting of PWAs with CircleCI and Firebase.
Happy Coding!
Fikayo Adepoju is a LinkedIn Learning (Lynda.com) Author, Full-stack developer, technical writer, and tech content creator proficient in Web and Mobile technologies and DevOps with over 10 years experience developing scalable distributed applications. With over 40 articles written for CircleCI, Twilio, Auth0, and The New Stack blogs, and also on his personal Medium page, he loves to share his knowledge to as many developers as would benefit from it. You can also check out his video courses on Udemy.