PWA with Next.js 🔥

Author : JaNakh Pon , August 23, 2021

Tags

What is a PWA(Progressive Web App) ?

PWA - Progressive Web App is a piece of technology, enabling businesses to build web applications that have the look and feel of native mobile apps. These apps are developed using web technologies like JavaScript, HTML, CSS. And further, developers utilize static site generators like Gatsby or VuePress to launch PWAs.

These apps run in the browser but give users the experience of a native app. And they can be:

  • Accessed through the mobile home screen
  • Give offline app access
  • Have a native feature; push notifications
  • And much more ...

How to build a PWA with Next.js

We are going to use the same repo from the previous article about next-seo:


Installation & setup

Let's get started by next-pwa dependency to our app:

  > cd next-seo-pwa && npm i next-pwa --save

And configure next-pwa in next.config.js:

const withPWA = require('next-pwa')

module.exports = withPWA({
  pwa: {
    dest: 'public',
    // disable: process.env.NODE_ENV === 'development',
    // register: true,
    // scope: '/app',
    // sw: 'service-worker.js',
    //...
  }
})

Implementation

Next, we need to create a file, named "manifest.json" and read more about it here.

Don't forget to add "purpose":"any maskable" to icon!:

manifest.json
{
  "name": "PWA",
  "short_name": "PWA",
  "display": "standalone",
  "orientation": "portrait",
  "theme_color": "#FFFFFF",
  "background_color": "#FFFFFF",
  "start_url": "/",
  "icons": [
    {
      "src": "/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
  }
  ]
}

Now we need to add more meta tags for PWA in _document.tsx:

_document.tsx
import Document, { Html, Head, Main, NextScript } from "next/document";
import React from "react";

const url = `http://localhost:3000/`;
const description = `HaHa | HeHe`
const name = "HaHa"
const social_image = "mstile-150x150.png"

export default class extends Document {
  static async getInitialProps(ctx: any) {
    return await Document.getInitialProps(ctx);
  }

  render() {
    return (
      <Html lang="en" dir="ltr">
        <Head>

          {/* HERE I AM 👇*/}
          <meta name='application-name' content='PWA App' />
          <meta name='apple-mobile-web-app-capable' content='yes' />
          <meta name='apple-mobile-web-app-status-bar-style' content='default' />
          <meta name='apple-mobile-web-app-title' content='PWA App' />
          <meta name='description' content='Best PWA App in the world' />
          <meta name='format-detection' content='telephone=no' />
          <meta name='mobile-web-app-capable' content='yes' />
          <meta name='msapplication-config' content='/icons/browserconfig.xml' />
          <meta name='msapplication-TileColor' content='#2B5797' />
          <meta name='msapplication-tap-highlight' content='no' />
          <meta name='theme-color' content='#000000' />


          <meta
            name="keywords"
            content="Hello, hola, blah blah"
          />
          <meta name="author" content="Ja Nakh Pon" />
          <meta name="robots" content="index,follow" />
          <meta name="googlebot" content="index,follow" />
          <meta name="description" content={description} />
          <meta name="theme-color" content="#006ABC" />

          {/* openGraph */}
          <meta property="og:title" content={name} />
          <meta
            property="og:description"
            content={description}
          />
          <meta property="og:type" content={"website"} />
          <meta property="og:url" content={url} />
          <meta property="og:locale" content="en_IE" />
          <meta property="og:site_name" content={name} />
          <meta property="og:image" content={social_image} />

          {/* Twitter */}
          <meta name="twitter:site" content={`@ja_nakh`} />
          <meta name="twitter:creator" content={`@ja_nakh`} />
          <meta name="twitter:card" content="summary_large_image" />
          <meta name="twitter:title" content={name} />
          <meta
            name="twitter:description"
            content={description}
          />
          <meta name="twitter:image" content={social_image} />

          {/* Icons */}
          <link rel="canonical" href={url} />
          <link rel="icon" href="/favicon.ico" />
          <link
            rel="apple-touch-icon"
            sizes="180x180"
            href="/apple-touch-icon.png"
          />
          <link
            rel="icon"
            type="image/png"
            sizes="32x32"
            href="/favicon-32x32.png"
          />
          <link
            rel="icon"
            type="image/png"
            sizes="16x16"
            href="/favicon-16x16.png"
          />
          
          {/* AND HERE 👇*/}
          <link rel="manifest" href="/manifest.json" />
          <link rel="shortcut icon" href="/favicon.ico" />
          <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
          <meta name="msapplication-TileColor" content="#da532c" />
          <meta name="theme-color" content="#006ABC" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

And now we need to add <title></title> and viewport to our _app.ts:

_app.js
import '../../styles/globals.css'
import type { AppProps } from 'next/app'
import Head from "next/head";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <title>PWA 🔥 </title>
        <meta
          name="viewport"
          content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, viewport-fit=cover"
        />
      </Head>
      <Component {...pageProps} />
    </>
  )
}
export default MyApp

And, One last thing ☝️, we can also add _offline.tsx for offline fallback:

_offline.tsx
import Head from "next/head";

const Offline = () => (
  <>
    <Head>
      <title>PWA</title>
    </Head>
    <h1>Oops! you are offline!</h1>
  </>
);
export default Offline;

Since everything is ready, let's build it and serve it in local. So, we could check the results using Lighthouse:

 > npm run build && npm run start
 > \\ Open Dev Tools > Lighthouse > Generate reports

and you should see something like this:

Lighthouse results

Congrats!!! now you have a PWA app with proper SEO tags 😉.


Source Code.