DEV Community

Vino Samari
Vino Samari

Posted on

No Internet, No Problem: Building a Progressive Web Application(PWA) with Nuxt.js that Works Anytime, Anywhere (Part 2).

πŸŽ‰ Welcome back to our series on building offline apps and PWAs with Nuxt.js! In the previous article, we introduced the power of Nuxt.js and discussed how it can help us create web apps that work great even without an internet connection. Today, we're excited to dive deeper into building Sportswire - a web app that delivers up-to-date sports news and analysis in a fun and engaging way πŸ†.

Our focus is setting up the frontend including basic assets and pages and learning how to configure Nuxt.js to package our web app to behave like a native app, so users can access Sportswire anytime, anywhere, even without an internet connection 🌎.


We will walk through each step of the Nuxt.js configuration, including the pwa module, service workers, caching strategies, and offline fallback pages, to build a PWA that works seamlessly offline πŸ’». Get ready to discover how Nuxt.js can make it easy for you to create PWAs with offline capabilities that deliver an exceptional user experience! πŸš€

Before we dive into the walkthrough, it's important to note that a few assumptions have been made. First, we assume that you have the latest/LTS version of node installed on your computer. Second, we assume that you have a basic understanding of Vue.js. Lastly, we assume that you have experience hosting a static website. If any of these assumptions do not apply to you, we recommend doing some research on the topics before proceeding.
<hr/

1. Nuxt.js project boilerplate

Let's set up our Nuxt frontend with all the necessary boilerplate code by running the following command in our terminal or command line interface.

npx create-nuxt-app sportswire
Enter fullscreen mode Exit fullscreen mode

Below are the resulting project initialization options. Some of these options are essential for our project, and others are optional, which we'll elaborate on later. It is recommended that you use similar values when initializing your project for more predictable results.

create-nuxt-app v4.0.0
✨  Generating Nuxt.js project in sportswire
? Project name: sportswire
? Programming language: JavaScript
? Package manager: Npm
? UI framework: None
? Nuxt.js modules: Axios - Promise based HTTP client, Progressive Web App (PWA)
? Linting tools: None
? Testing framework: None
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Static (Static/Jamstack hosting)
? Development tools: jsconfig.json (Recommended for VS Code if you're not using typescript)
? What is your GitHub username? vinosamari
? Version control system: Git
β Έ Installing packages with npm ...

πŸŽ‰  Successfully created project sportswire

  To get started:

        cd sportswire
        npm run dev

  To build & start for production:

        cd sportswire
        npm run build
        npm run start

Enter fullscreen mode Exit fullscreen mode

🚨IMPORTANT: *MAKE SURE YOU SELECT Progressive Web App (PWA) UNDER Nuxt.js modules(There will be a setup instructions for already initialized Nuxt projects later in the article). *We have selected Tailwind CSS as the UI framework option, but other frameworks also work well. However, all the custom Tailwind classes will still be included in their vanilla CSS form in the project's global CSS file.*Linting and testing tools are not covered in this walkthrough.

2. Nuxt config

The next step is to navigate into the sportswire directory (cd sportswire) and open it in your favourite code editor or IDE (for me that's VS Code with code .). In the root directory(project folder) you will find a nuxt.config.js file. This file is a core configuration file for your Nuxt application. It contains a variety of options that determine the behavior of your application, such as the css, plugins, server settings and build options. I have displayed my current nuxt.config.js file below, which will help you to better understand the configuration options for this project and how to modify them in yours.


//nuxt.config.js

export default {
  // Target: https://go.nuxtjs.dev/config-target
  target: "static",

  // Global page headers: https://go.nuxtjs.dev/config-head
  head: {
    title: "SportsWire - Up-to-date Sports News and Analysis App",
    meta: [
      { charset: "utf-8" },
      { name: "viewport", content: "width=device-width, initial-scale=1" },
      {
        hid: "description",
        name: "description",
        content:
          "Stay up-to-date with the latest sports news and analysis anytime, anywhere with Sportswire - a fun and engaging web app built with Nuxt.js.",
      },
      { name: "format-detection", content: "telephone=no" },
    ],
    link: [{ rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
  },

  // Global CSS: https://go.nuxtjs.dev/config-css
  css: ["@/assets/css/styles.css"],

  // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
  plugins: [],

  // Auto import components: https://go.nuxtjs.dev/config-components
  components: true,

  // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
  buildModules: [
    // https://go.nuxtjs.dev/tailwindcss
    "@nuxtjs/tailwindcss",
  ],

  // Modules: https://go.nuxtjs.dev/config-modules
  modules: [
    // https://go.nuxtjs.dev/axios
    "@nuxtjs/axios",
    // https://go.nuxtjs.dev/pwa
    "@nuxtjs/pwa",
  ],

  // Axios module configuration: https://go.nuxtjs.dev/config-axios
  axios: {
    // Workaround to avoid enforcing hard-coded localhost:3000: https://github.com/nuxt-community/axios-module/issues/308
    baseURL: "/",
  },

  // PWA module configuration: https://go.nuxtjs.dev/pwa
  pwa: {
    manifest: {
      lang: "en",
    },
  },

  // Build Configuration: https://go.nuxtjs.dev/config-build
  build: {},
};

Enter fullscreen mode Exit fullscreen mode


Keep a few things in mind:

  • Your title and description meta tags will be automatically generated by default, but you can customize them(like i did) to better suit the project.Check out the official Nuxt.js documentation to learn more.
  • If you're using a UI framework, you may need to include it as a build module in your nuxt.config.js file or it will be added if you opt for it at initialization. In my case, i'm using the "@nuxtjs/tailwindcss" module.
  • I have added a path to my css property @/assets/css/styles.css. This points to my global css file which i created in projectFolder/assets/css/styles.css for styling purposes. You can, of course, ignore this step and style your components as you normally would at no peril to the project.
  • If you have an already existing nuxt.config project, there may or may not be a pwa object, depending on your choices at initialization but don't worry. We'll get to setting up for you in a bit.

πŸ“ Note: At this point, serving your app on your localhost/local server with npm run dev in your terminal should display the default Nuxt.js landing page at 'http://localhost:3000' in your web browser. You can serve it to check that your configuration is error-free.

3. PWA module config

If you already have an existing Nuxt.js project you want to install the PWA module in, run npm i --save-dev @nuxtjs/pwa
. Read more setup instructions in the docs.
After installation, edit your nuxt.config.js file to include the PWA module.

{
  buildModules: [
    '@nuxtjs/pwa',
  ]
}
// NOTE: If using `ssr: false` with production mode without `nuxt generate`, you have to use `modules` instead of `buildModules`

When our web app is added to the users' devices we need accompanying app icons and splash screens for it to look and feel native . For this we ensure that;

  • There is a static/ directory in the project's root folder.
  • It contains a static/icon.png which should be a square .png image with dimensions >= 512x512px. This is the file the module uses to generate the various app icons and splash screens for your PWA.You can check Flaticon[ ](https://favicon.io) for ideas.

You can find more details [in the module docs].(https://pwa.nuxtjs.org/) For reference i have displayed my PWA module config for this project below.

//nuxt.config.js

// PWA module configuration: https://go.nuxtjs.dev/pwa
  pwa: {
    // https://pwa.nuxtjs.org/manifest
    // Manifest adds Web App Manifest with no pain.
    manifest: {
      name: "SportsWire",
      short_name: "SportsWire",
      description: "Up-to-date Sports News and Analysis App.",
      theme_color: "#6a5acd",
      lang: "en",
      background_color: "#6a5acd",
    },
    // https://pwa.nuxtjs.org/icon
    icon: {
      sizes: [64, 120, 144, 152, 192, 384, 512], //Array of sizes to be generated (Square).These are the default values
    },
    // https://pwa.nuxtjs.org/meta
    // Meta easily adds common meta tags into your project with zero-config needed. 
    meta: {
      name: "SportsWire",
      description: "Up-to-date Sports News and Analysis App.",
      author: "Vino Samari",
      theme_color: "#6a5acd",
      nativeUi: true,
      appleStatusBarStyle: "black",
      mobileAppIOS: true,
    },
  },
Enter fullscreen mode Exit fullscreen mode

So far, the home page looks something like this;
HomePage

The index.vue page looks something like;

<template>
  <div class="wrapper">
    <h1 class="header">Welcome to SportsWire!</h1>
    <h2 class="subheader">Your Up-to-date Sports News and Analysis App.</h2>
    <section class="card" v-for="news in dummyData" :key="news.imgUrl">
      <img :src="news.imgUrl" :alt="news.source" class="avatar" />
      <h1 class="header">{{ news.headline }}</h1>
      <h2 class="subheader">{{ news.source }}</h2>
      <p class="content">{{ news.body }}</p>
      <button class="btn">Read Full Article</button>
    </section>
  </div>
</template>

<script>
export default {
  name: "IndexPage",
  data() {
    return {
      dummyData: [
        {
          headline: "Someone said something",
          source: "He",
          body: "Someone said something that sparked a flurry of excitement and anticipation among a small group of enthusiasts yesterday. The comment, which was made during an obscure online forum discussion, hinted at the possibility of a major breakthrough in a long-standing scientific mystery.Many experts in the field were quick to jump on the news, speculating on what the breakthrough could mean for the future of their discipline. The media also picked up on the story, with various outlets running headlines proclaiming the imminent discovery of something groundbreaking.However, as the hours turned into days, it became clear that there was no substance behind the original comment. In fact, the person who made the statement later admitted that it was nothing more than a passing thought that they had shared in the heat of the moment.The disappointment was palpable among the community that had been so excited just a few days earlier. Some criticized the media for running with a story without verifying its authenticity, while others expressed frustration at the person who had made the comment in the first place.Ultimately, though, the episode served as a reminder of the dangers of speculation and hype in the world of science.",
          imgUrl:
            "https://images.pexels.com/photos/2608517/pexels-photo-2608517.jpeg?auto=compress&cs=tinysrgb&w=400",
        },
        {
          headline: "Something was said somewhere",
          source: "She",
          body: "Something was said somewhere that has left many people scratching their heads and wondering what it could mean. The comment, which was reportedly made by a high-ranking government official during a closed-door meeting, has been the subject of intense speculation in political circles.Some have interpreted the remark as a sign that major policy changes are on the horizon, while others believe it could be a coded message to a foreign government. There are even those who think the comment was simply a slip of the tongue and should not be taken seriously.Despite the rampant speculation, no one seems to have a clear idea of what the comment was actually referring to. The government official who made the remark has not commented publicly on the matter, and it is unclear whether they will do so in the future.",
          imgUrl:
            "https://images.pexels.com/photos/4969845/pexels-photo-4969845.jpeg?auto=compress&cs=tinysrgb&w=400",
        },
        {
          headline: "Somehow, somewhere, something happened",
          source: "They",
          body: "Somehow, somewhere, something happened that has left experts in a state of confusion and bewilderment. The incident, which appears to have taken place in a remote corner of the world, involves a mysterious phenomenon that defies explanation.According to their report, a strange energy field appeared out of nowhere, hovering above the ice and snow for several hours before disappearing just as suddenly. The field appeared to be emitting a high-frequency signal that could not be detected by any of the instruments the researchers had brought with them.",
          imgUrl:
            "https://images.pexels.com/photos/3810756/pexels-photo-3810756.jpeg?auto=compress&cs=tinysrgb&w=400",
        },
      ],
    };
  },
};
</script>


<style>
/*
These are the TailwindCSS custom classes. However, We are using their vanilla css forms from `assets/css/styles.css` for this tutorial.

.wrapper {
  @apply container tracking-wider text-center p-2;
}
.header {
  font-family: "Barlow", sans-serif;
  @apply font-bold text-4xl rounded-3xl shadow-xl  my-10 p-5 text-purple-600;
}
.subheader {
  font-family: "Ubuntu", sans-serif;
  @apply text-xl font-light underline;
}
.avatar {
  @apply rounded-full w-24 h-24 object-cover;
}
.card {
  @apply shadow-sm hover:shadow-xl transform transition-all duration-300 ease-in-out rounded w-5/6 my-8 text-left py-5 px-3 mx-auto cursor-pointer;
}
.card .header {
  @apply w-1/2 my-1 rounded-sm text-2xl shadow-none p-0;
}
.card .subheader {
  @apply text-xs;
}
.content {
  @apply my-5 text-xs;
}
.btn {
  @apply bg-purple-600 text-white uppercase font-bold tracking-wider rounded-2xl m-3 px-6 py-2 text-sm;
}
.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
*/
</style>
Enter fullscreen mode Exit fullscreen mode

This is what the styles.css file looks like with all the vanilla css forms of the TailwindCSS custom classes. NOTE: THIS IS JUST FOR THE PURPOSE OF CONTINUITY IN REPLICATION OF THIS TUTORIAL NOT A REPRESENTATION OF THE ACTUAL PROJECT.

@import url("https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;0,700;1,300;1,400;1,500;1,700&display=swap");

.wrapper {
  display: block;
  margin-left: auto;
  margin-right: auto;
  padding-top: 2rem;
  padding-bottom: 2rem;
  padding-left: 0.5rem;
  padding-right: 0.5rem;
  text-align: center;
  font-size: 1.125rem;
  line-height: 1.5;
  letter-spacing: 0.025em;
  max-width: 100%;
}
.avatar {
  border-radius: 9999px;
  width: 6rem;
  height: 6rem;
  object-fit: cover;
}
.card {
  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0 1px 3px 1px rgba(0, 0, 0, 0.1);
  transform-origin: center center;
  transition-property: box-shadow, transform;
  transition-duration: 0.3s;
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  border-radius: 0.25rem;
  width: 83.333333%;
  margin-top: 2rem;
  margin-bottom: 2rem;
  text-align: left;
  padding-top: 1.25rem;
  padding-bottom: 1.25rem;
  padding-left: 0.75rem;
  padding-right: 0.75rem;
  margin-left: auto;
  margin-right: auto;
  cursor: pointer;
}
.card:hover {
  box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.2),
    0 1px 3px 1px rgba(0, 0, 0, 0.1);
  transform: scale(1.05);
  background-color: #a78bfa;
  color: #fff;
}
.card .header {
  font-family: "Ubuntu", sans-serif;
  margin-top: 0.25rem;
  margin-bottom: 0.25rem;
  border-radius: 0.125rem;
  font-size: 1.5rem;
  box-shadow: none;
  padding-top: 0;
  padding-bottom: 0;
  padding-left: 0;
  padding-right: 0;
}

.card .subheader {
  font-size: 0.75rem;
}
.header {
  font-family: "Barlow", sans-serif;
  font-weight: 700;
  font-size: 2.25rem;
  line-height: 2.5rem;
  border-radius: 1.5rem;
  box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
  margin-top: 2.5rem;
  margin-bottom: 2.5rem;
  padding-top: 1.25rem;
  padding-bottom: 1.25rem;
  padding-left: 1.25rem;
  padding-right: 1.25rem;
}
.subheader {
  font-family: "Ubuntu", sans-serif;
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: 0.1em;
}
.content {
  margin-top: 1.25rem;
  margin-bottom: 1.25rem;
  font-size: 0.75rem;
}
.btn {
  border-color: transparent;
  background-color: #9f7aea;
  color: #fff;
  text-transform: uppercase;
  font-weight: 700;
  letter-spacing: 0.1em;
  border-radius: 0.375rem;
  margin-top: 0.75rem;
  margin-bottom: 0.75rem;
  padding-left: 1.5rem;
  padding-right: 1.5rem;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  font-size: 0.875rem;
}
.btn:hover {
  background-color: #8b5cf6;
}
.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

Enter fullscreen mode Exit fullscreen mode

There you go! A Nuxt.js. Progressive Web Application with all the basic functionalities. A quick lighthouse audit for PWA should indicate how much abstraction the Nuxt.js PWA module handles for all service workers, web manifests, and required assets for your web app. With this setup, once deployed, your browser should indicate that your web app is installable and even prompt you to do so in some browsers on some devices. Francesco Leardini has an in-depth article on user pwa installation for further reading. iPhones(Safari) will usually present a "Add To Home Page" button in the browser's share menu. The process is fairly similar in Chrome and on Android devices.

The next part of this series will focus on fetching real news from an API, caching data and assets for offline accessibility and improving the quality of life of users with elements like helper pop-ups to guide them through installing the web app on first-time visit.

The github repository for the project is available here.. Feel free to leave a comment on something i might have missed.

Cheers!

Top comments (0)