Building a Github Repo Template Part 7: Next.js PWA Support

In this ongoing series, I am putting together a Github Repository Template for my "Go To" front-end tech stack of Next.js, React, TypeScript etc. In this part of the series, I'm going to add basic Progressive Web App support and get a baseline Lighthouse score leveraging next-offline.

Just want the Code? View the full changeset covered in this post or check out the 0.0.7 release of the repository.

Prerequisites

Initial Lighthouse Scoring

Before we do anything, we need to generate an initial Lighthouse audit report so we know what we're trying to improve upon. To get a worthwhile Lighthouse score in general, the audits should really be run against a production build so I'll start there.

npm run build && npm start

I'll now open the app in Chrome. To run the Lighthouse audits, open the Chrome Developer tools and find the Lighthouse tab.

Screen Shot 2020-07-24 at 12.11.51 PM.png

Clicking "Generate report" kicks off the audits. The final results are: Screen Shot 2020-07-24 at 12.16.53 PM.png

This is a really good score. Albeit the application does barely anything. Note, however, I do not have a PWA score. The section for PWA: Screen Shot 2020-07-24 at 12.19.45 PM.png

Most of these issues are due to missing two key things: a service worker and a manifest.json. I'll put my energy into the PWA setup and revisit the other warnings shortly. Here is the overview of the game plan:

  • Install next-offline
  • Add an express server handler to serve up the service worker.
  • Create a manifest.json
  • Spot fix various warnings

Installing Next Offline support

Luckily, a lot of what I need to add PWA support to Next.js is nicely packaged into next-offline. I'm using the latest version 5.0.2.

Install next-offline:

npm install --save next-offline

Using Express to serve our Service Worker

PWA support relies on service workers. Service Workers have to be served from the same domain as your project. Next.js does support a public directory, but I need something like the express server to route the request. It is annoying, but I tend to always have an express server anyway.

Installing Express and Setting up Server

Because service workers only apply to the directory tree they are served from and we want the service worker to apply to everything below the web root, we're going to need to serve the service worker from the web root (i.e. /service-worker.js). In order to do this, we're going to need to a custom server to handle the route. We can accomplish this with express.

Note: Depending on your hosting, you might be able to avoid this step. For example, if we were hosting on Google App Engine, we could use the app.yaml handler directive to accomplish this. However, it is very vendor specific and typically only works when deployed. I want to be able to test locally. Also, I use express a lot for other things.

To get started, I'll install express:

npm install express --save

Now I'll create a simple server.js in my project root with the handlers we will need for the service worker and to allow Next.js to continue doing its thing.

/* eslint-disable @typescript-eslint/no-var-requires */
// Main Entry point of app
const express = require('express');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const port = process.env.PORT ? process.env.PORT : 3000;

app
  .prepare()
  .then(() => {
    const server = express();

    // Handle Service Worker
    server.get('/service-worker.js', (_req, res) => {
      res.status(200).sendFile('/service-worker.js', { root: `${__dirname}/.next/` });
    });

    // Route Everything else through Next.js directly
    server.all('*', (req, res) => handle(req, res));

    server.listen(port, () => {
      // eslint-disable-next-line no-console
      console.log(`> Ready on http://localhost:${port}`);
    });
  })
  .catch((ex) => {
    // eslint-disable-next-line no-console
    console.error(ex.stack);
    process.exit(1);
  });

Next, I need to update my dev and start scripts in package.json

    "dev": "node ./server.js",
    "start": "NODE_ENV=production PORT=8080 node ./server.js",

That was a big change. Here is what is going on:

  • We are no longer calling next directly inside of our dev and start script. Instead, we are having node run our ./server.js script
  • The server.js script, rather than the package.json script, loads next and routes traffic to it.
  • For the start script, the -p port argument applies to Next.js specifically, which is not what we are running now. However, as I most often use Google App Engine for hosting, I wanted to leverage their PORT environment variable. This is read and passed into the express server listen().
  • The next-offline package will build the service worker for us and put it in the build directory for Next.js. By default, this is /.next. We use express to intercept the /service-worker.js route and serve up the file located within the /.next/ directory
  • Lastly, it is worth noting that the server.js is lower level than the Next.js application. As such, it doesn't have all the TypeScript niceties. As such, it is written in vanilla ES6 (re: the require syntax) which is natively interpreted by Node. If you need the server to be in TypeScript for various reasons, move it to the ./src directory and consider using nodemon to enable code refresh, etc. I do this for several projects, but it is not universal, so I will not include it here.

Configuring Next.js for next-offline

Now that next-offline is installed I need to configure Next.js to use it. To do this, I'll update my /next.config.js file I introduced in previous steps.

/* eslint-disable @typescript-eslint/no-var-requires */
// Next Config
const withOffline = require('next-offline');

const nextConfig = {
  serverRuntimeConfig: {},
  publicRuntimeConfig: {
    // Will be available on both server and client
    greeting_emoji: process.env.NEXT_PUBLIC_THEME_GREETING_EMOJI,
  },
};
module.exports = withOffline(nextConfig);

Create a manifest.json

The last step before we can get any PWA support is to add a manifest.json file with some basic necessities. Luckily, this file can be loaded from any url, so we can leverage Next.js static files.

In the project root, I will create the directory /public/static. Inside the static directory, I will create the manifest.json file with the following contents.

{
    "short_name": "CHANGE ME",
    "name": "Change ME BEFORE DEPLOYING",
    "icons":[
        {
            "src":"/static/android-chrome-192x192.png",
            "sizes":"192x192",
            "type":"image/png"
        },
        {
            "src":"/static/android-chrome-512x512.png",
            "sizes":"512x512",
            "type":"image/png"
        }
    ],
    "start_url": "/?source=pwa",
    "background_color": "#000000",
    "display": "standalone",
    "scope": "/",
    "theme_color": "#000000"
  }

Note: The static image assets referenced do not exist yet. However, I'll get to that in a bit.

Finally, update the Next.js <Head> tag in your /pages/_document.tsx file to include a link to the manifest.json

<link rel="manifest" href="/static/manifest.json" />

Now when I rebuild and run the production app and re-run the Lighthouse audit: Screen Shot 2020-07-24 at 2.52.12 PM.png

We now have PWA support! Celebrate! Also, note that we can install our application as well. Screen Shot 2020-07-24 at 4.19.44 PM.png Be sure to change the name values in manifest.json 😃

Final Steps

I'm not going to go into every individual step to resolve the remainder of the issues. There are plenty of guides for that.

I will address one thing: Favicon and App Icons In the manifest.json, there was a list of static icon assets. I like to use this tool to generate my favicons and app icons. Just put the output in the /public/static directory and reference via the /static url in the app.

Closing Thoughts

I now have a pretty solid template with most of the basic things I'm going to need as a good starting point. View the full changeset covered in this post or check out the 0.0.7 release of the repository.

With this part of the series complete, I am cutting the 1.0 release. Feel free to use it for your projects and give me feedback. I hope you find it helpful.

Note that I didn't include a lot of things I might want: Storybook, Firebase, Redux Toolkit, React Testing Library, etc. This is by design for now. Some of the projects I spin up utilize these things and others do not.

Image Credit: Photo by Stella Schafer from Pexels

Comments (1)

Edidiong Asikpo's photo

This is amazing Blaine Garrett. Thanks for sharing.