Converting Your Website to a PWA: A Comprehensive Guide

Building web and mobile solutions has been my passion for years, but the moment I converted my first website into a Progressive Web App (PWA), I realized I had been missing out on a whole new dimension of user experience. As a senior developer based in Port Harcourt, Nigeria, where internet connectivity can sometimes be unreliable, I've seen firsthand how PWAs can transform user engagement by providing offline capabilities, faster load times, and native-like experiences. In this guide, I'll walk you through the complete process of converting both traditional websites and framework-based applications into PWAs, sharing useful examples. What are Progressive Web Apps? Progressive Web Apps are web applications that use modern web capabilities to deliver app-like experiences to users. They work offline, can be installed on home screens, and provide features like push notifications—all without the friction of app store distribution. Let me share why PWAs matter with a quick story: Last year, I worked with a local business here in Port Harcourt that was struggling with customer engagement. After converting their simple website to a PWA, they saw a 70% increase in return visitors and a significant uptick in sales, particularly during internet outages when competitors' websites were inaccessible. Converting Traditional HTML/CSS/JavaScript Websites to PWAs Step 1: Create a Web App Manifest The manifest.json file tells browsers how your app should behave when installed. Here's a basic example I use for most of my HTML/CSS/JavaScript projects: { "name": "TegaTech Solutions", "short_name": "TegaTech", "start_url": "/index.html", "display": "standalone", "background_color": "#ffffff", "theme_color": "#4A90E2", "description": "Building scalable solutions for Nigerian businesses", "icons": [ { "src": "images/icon-192.png", "sizes": "192x192", "type": "image/png" }, { "src": "images/icon-512.png", "sizes": "512x512", "type": "image/png" } ] } Add this to your HTML: Step 2: Add iOS Support Apple devices require additional meta tags: Step 3: Implement a Service Worker Service workers enable the offline capabilities that make PWAs so powerful. Here's a simplified version: // service-worker.js const CACHE_NAME = 'tegate-cache-v1'; const urlsToCache = [ '/', '/index.html', '/css/styles.css', '/js/main.js', '/images/logo.png', // Add other assets you want to cache ]; // Installation - caches assets self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Cache opened successfully'); return cache.addAll(urlsToCache); }) ); }); // Fetch - serve from cache, fallback to network self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => { // Return cached response if found if (response) { return response; } // Clone the request for the fetch call const fetchRequest = event.request.clone(); return fetch(fetchRequest).then(response => { // Don't cache non-successful responses or non-GET requests if (!response || response.status !== 200 || response.type !== 'basic' || event.request.method !== 'GET') { return response; } // Clone the response for caching const responseToCache = response.clone(); caches.open(CACHE_NAME) .then(cache => { cache.put(event.request, responseToCache); }); return response; }); }) ); }); // Activate - clean up old caches self.addEventListener('activate', event => { const cacheWhitelist = [CACHE_NAME]; event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); } }) ); }) ); }); Register the service worker in your main JavaScript file: // In your main.js or inline in HTML if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then(registration => { console.log('ServiceWorker registered successfully:', registration.scope); }) .catch(error => { console.log('ServiceWorker registration failed:', error); }); }); } Step 4: Implement "Add to Home Screen" Experience I found that customizing the install experience significantly improves installation rates: let deferredPrompt; // Listen for the beforeinstallprompt event window.addEventListener('beforeinstallprompt', (e) => { // Prevent Chrome 67+ from automatically showing the prompt e.preventDefault();

May 18, 2025 - 13:10
 0
Converting Your Website to a PWA: A Comprehensive Guide

Building web and mobile solutions has been my passion for years, but the moment I converted my first website into a Progressive Web App (PWA), I realized I had been missing out on a whole new dimension of user experience. As a senior developer based in Port Harcourt, Nigeria, where internet connectivity can sometimes be unreliable, I've seen firsthand how PWAs can transform user engagement by providing offline capabilities, faster load times, and native-like experiences.

In this guide, I'll walk you through the complete process of converting both traditional websites and framework-based applications into PWAs, sharing useful examples.

What are Progressive Web Apps?

Progressive Web Apps are web applications that use modern web capabilities to deliver app-like experiences to users. They work offline, can be installed on home screens, and provide features like push notifications—all without the friction of app store distribution.

Let me share why PWAs matter with a quick story: Last year, I worked with a local business here in Port Harcourt that was struggling with customer engagement. After converting their simple website to a PWA, they saw a 70% increase in return visitors and a significant uptick in sales, particularly during internet outages when competitors' websites were inaccessible.

Converting Traditional HTML/CSS/JavaScript Websites to PWAs

Step 1: Create a Web App Manifest

The manifest.json file tells browsers how your app should behave when installed. Here's a basic example I use for most of my HTML/CSS/JavaScript projects:

{
  "name": "TegaTech Solutions",
  "short_name": "TegaTech",
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#4A90E2",
  "description": "Building scalable solutions for Nigerian businesses",
  "icons": [
    {
      "src": "images/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "images/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Add this to your HTML:

 rel="manifest" href="/manifest.json">

Step 2: Add iOS Support

Apple devices require additional meta tags:

 name="apple-mobile-web-app-capable" content="yes">
 name="apple-mobile-web-app-status-bar-style" content="black">
 name="apple-mobile-web-app-title" content="TegaTech">
 rel="apple-touch-icon" href="/images/icon-152.png">

Step 3: Implement a Service Worker

Service workers enable the offline capabilities that make PWAs so powerful. Here's a simplified version:

// service-worker.js
const CACHE_NAME = 'tegate-cache-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/css/styles.css',
  '/js/main.js',
  '/images/logo.png',
  // Add other assets you want to cache
];

// Installation - caches assets
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Cache opened successfully');
        return cache.addAll(urlsToCache);
      })
  );
});

// Fetch - serve from cache, fallback to network
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Return cached response if found
        if (response) {
          return response;
        }

        // Clone the request for the fetch call
        const fetchRequest = event.request.clone();

        return fetch(fetchRequest).then(response => {
          // Don't cache non-successful responses or non-GET requests
          if (!response || response.status !== 200 || response.type !== 'basic' || event.request.method !== 'GET') {
            return response;
          }

          // Clone the response for caching
          const responseToCache = response.clone();

          caches.open(CACHE_NAME)
            .then(cache => {
              cache.put(event.request, responseToCache);
            });

          return response;
        });
      })
  );
});

// Activate - clean up old caches
self.addEventListener('activate', event => {
  const cacheWhitelist = [CACHE_NAME];
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheWhitelist.indexOf(cacheName) === -1) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

Register the service worker in your main JavaScript file:

// In your main.js or inline in HTML
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then(registration => {
        console.log('ServiceWorker registered successfully:', registration.scope);
      })
      .catch(error => {
        console.log('ServiceWorker registration failed:', error);
      });
  });
}

Step 4: Implement "Add to Home Screen" Experience

I found that customizing the install experience significantly improves installation rates:

let deferredPrompt;

// Listen for the beforeinstallprompt event
window.addEventListener('beforeinstallprompt', (e) => {
  // Prevent Chrome 67+ from automatically showing the prompt
  e.preventDefault();

  // Stash the event so it can be triggered later
  deferredPrompt = e;

  // Update UI to notify the user they can add to home screen
  const installBanner = document.getElementById('installBanner');
  if (installBanner) {
    installBanner.style.display = 'flex';
  }
});

// Setup the install button click handler
document.getElementById('installBtn').addEventListener('click', async () => {
  // Hide our user interface that shows our install button
  document.getElementById('installBanner').style.display = 'none';

  // Show the prompt
  deferredPrompt.prompt();

  // Wait for the user to respond to the prompt
  const { outcome } = await deferredPrompt.userChoice;
  console.log(`User response: ${outcome}`);

  // We've used the prompt, and can't use it again, throw it away
  deferredPrompt = null;
});

// Listen for successful installation
window.addEventListener('appinstalled', (event) => {
  console.log('App was installed to home screen');
  // Analytics tracking
  gtag('event', 'pwa_installed');
});

The corresponding HTML:

 id="installBanner" class="install-banner" style="display: none;">
  

Install TegaTech for a better experience! id="installBtn">Install Now id="dismissBtn" onclick="this.parentNode.style.display='none'">Maybe Later

And some CSS I typically use:

.install-banner {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: #f8f9fa;
  color: #333;
  padding: 16px;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: 'Segoe UI', sans-serif;
  z-index: 1000;
}

.install-banner button {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: bold;
}

#installBtn {
  background: #4A90E2;
  color: white;
}

#dismissBtn {
  background: transparent;
  color: #666;
}

Step 5: Test Your Traditional PWA

After implementing the above steps on a local business site, I always test using Lighthouse in Chrome DevTools:

  1. Open Chrome DevTools (F12)
  2. Go to the "Lighthouse" tab
  3. Check "Progressive Web App" in the categories
  4. Click "Generate report"

I've learned the hard way that fixing issues early saves a lot of headache later. For example, on a previous project, I found that some assets weren't being cached correctly, which was causing the PWA to work inconsistently offline.

Converting React and Vue Applications to PWAs

Framework-based applications follow similar principles but with some framework-specific approaches.

React Implementation

For React applications, I typically use Create React App (CRA) which has built-in PWA support:

  1. If starting a new project:
npx create-react-app my-pwa --template cra-template-pwa
  1. For existing projects, add PWA support:
npm install workbox-webpack-plugin --save-dev
  1. In your src/index.js, modify the service worker registration:
// Before React 18
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note: this comes with some pitfalls.
// Learn more about service workers: https://cra.link/PWA
serviceWorkerRegistration.register();
  1. Customize the manifest.json in the public folder:
{
  "short_name": "NigerTech",
  "name": "Nigerian Tech Solutions by Adiri",
  "icons": [
    {
      "src": "favicon.ico",
      "sizes": "64x64",
      "type": "image/x-icon"
    },
    {
      "src": "logo192.png",
      "type": "image/png",
      "sizes": "192x192"
    },
    {
      "src": "logo512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ],
  "start_url": ".",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff"
}
  1. For custom "Add to Home Screen" prompts in React, I create a reusable component:
import React, { useState, useEffect } from 'react';
import './InstallPrompt.css';

const InstallPrompt = () => {
  const [installPrompt, setInstallPrompt] = useState(null);
  const [showBanner, setShowBanner] = useState(false);

  useEffect(() => {
    window.addEventListener('beforeinstallprompt', (e) => {
      // Prevent the mini-infobar from appearing on mobile
      e.preventDefault();
      // Stash the event so it can be triggered later
      setInstallPrompt(e);
      // Check if user has engaged enough with the app
      if (hasUserEngagedEnough()) {
        setShowBanner(true);
      }
    });

    window.addEventListener('appinstalled', () => {
      // Log install to analytics
      console.log('PWA was installed');
      setShowBanner(false);
    });

    return () => {
      window.removeEventListener('beforeinstallprompt', () => {});
      window.removeEventListener('appinstalled', () => {});
    };
  }, []);

  const hasUserEngagedEnough = () => {
    // For simplicity, we're just checking if they've visited before
    return localStorage.getItem('visited') === 'true';
  };

  const handleInstallClick = async () => {
    if (!installPrompt) return;

    // Show the install prompt
    installPrompt.prompt();

    // Wait for the user to respond to the prompt
    const { outcome } = await installPrompt.userChoice;
    console.log(`User response to the install prompt: ${outcome}`);

    // We've used the prompt, and can't use it again
    setInstallPrompt(null);
    setShowBanner(false);
  };

  const handleDismiss = () => {
    setShowBanner(false);
    // Remember dismissal in localStorage
    localStorage.setItem('installPromptDismissed', Date.now().toString());
  };

  if (!showBanner) return null;

  return (
    <div className="install-banner">
      <p>Get the best experience with our app!p>
      <div className="banner-buttons">
        <button onClick={handleInstallClick} className="install-btn">
          Install Now
        button>
        <button onClick={handleDismiss} className="dismiss-btn">
          Not Now
        button>
      div>
    div>
  );
};

export default InstallPrompt;

I then include this component in my App.js:

import React, { useEffect } from 'react';
import InstallPrompt from './components/InstallPrompt';
import './App.css';

function App() {
  useEffect(() => {
    // Mark that user has visited the site
    localStorage.setItem('visited', 'true');
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <h1>TegaTech Solutionsh1>
        <p>Building the future, one app at a timep>
      header>
      <main>
        {/* Your app content */}
      main>
      <InstallPrompt />
    div>
  );
}

export default App;

Vue Implementation

For Vue.js projects, I use the PWA plugin:

  1. For new projects:
vue create my-pwa
cd my-pwa
vue add pwa
  1. For existing projects:
vue add pwa
  1. After adding the plugin, you can customize the PWA settings in the vue.config.js file:
// vue.config.js
module.exports = {
  pwa: {
    name: 'TegaTech App',
    themeColor: '#4A90E2',
    msTileColor: '#000000',
    appleMobileWebAppCapable: 'yes',
    appleMobileWebAppStatusBarStyle: 'black',
    workboxPluginMode: 'GenerateSW',
    workboxOptions: {
      skipWaiting: true,
      clientsClaim: true,
      exclude: [/\.map$/, /_redirects/],
      runtimeCaching: [
        {
          urlPattern: new RegExp('^https://api\\.myapp\\.com/'),
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api-cache',
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 60 * 60 * 24, // 1 day
            },
          },
        },
        {
          urlPattern: new RegExp('^https://fonts\\.googleapis\\.com/'),
          handler: 'StaleWhileRevalidate',
          options: {
            cacheName: 'google-fonts-stylesheets',
          },
        },
      ],
    },
    iconPaths: {
      favicon32: 'img/icons/favicon-32x32.png',
      favicon16: 'img/icons/favicon-16x16.png',
      appleTouchIcon: 'img/icons/apple-touch-icon.png',
      maskIcon: 'img/icons/safari-pinned-tab.svg',
      msTileImage: 'img/icons/mstile-150x150.png'
    }
  }
}
  1. Create a custom install component in Vue:

<template>
   v-if="showBanner" class="install-banner">
    

Install our app for the best experience in Port Harcourt! class="banner-buttons"> @click="handleInstall" class="install-btn">Install @click="handleDismiss" class="dismiss-btn">Not Now