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();

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!
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:
- Open Chrome DevTools (F12)
- Go to the "Lighthouse" tab
- Check "Progressive Web App" in the categories
- 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:
- If starting a new project:
npx create-react-app my-pwa --template cra-template-pwa
- For existing projects, add PWA support:
npm install workbox-webpack-plugin --save-dev
- 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();
- Customize the
manifest.json
in thepublic
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"
}
- 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:
- For new projects:
vue create my-pwa
cd my-pwa
vue add pwa
- For existing projects:
vue add pwa
- 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'
}
}
}
- 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">
template>
<script>
export default {
name: 'InstallPrompt',
data() {
return {
deferredPrompt: null,
showBanner: false
}
},
mounted() {
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
this.deferredPrompt = e;
// Check if user should see the banner
if (this.hasUserEngaged()) {
this.showBanner = true;
}
});
window.addEventListener('appinstalled', () => {
console.log('App installed successfully');
this.showBanner = false;
this.$emit('app-installed');
});
},
methods: {
hasUserEngaged() {
// Simple engagement check
return localStorage.getItem('pageVisits') >= 2;
},
async handleInstall() {
if (!this.deferredPrompt) return;
this.deferredPrompt.prompt();
const { outcome } = await this.deferredPrompt.userChoice;
console.log(`Installation outcome: ${outcome}`);
this.deferredPrompt = null;
this.showBanner = false;
},
handleDismiss() {
this.showBanner = false;
localStorage.setItem('installDismissed', Date.now().toString());
}
},
beforeDestroy() {
window.removeEventListener('beforeinstallprompt', () => {});
window.removeEventListener('appinstalled', () => {});
}
}
script>
<style scoped>
.install-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #f8f9fa;
padding: 16px;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1000;
}
.banner-buttons {
display: flex;
gap: 10px;
}
.install-btn {
padding: 8px 16px;
background: #4A90E2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.dismiss-btn {
padding: 8px 16px;
background: transparent;
color: #666;
border: none;
border-radius: 4px;
cursor: pointer;
}
style>
Use this component in your App.vue:
<template>
id="app">
@app-installed="trackInstallation" />
template>
<script>
import InstallPrompt from './components/InstallPrompt.vue';
export default {
name: 'App',
components: {
InstallPrompt
},
mounted() {
// Track page visits for engagement metrics
const visits = parseInt(localStorage.getItem('pageVisits') || '0');
localStorage.setItem('pageVisits', visits + 1);
},
methods: {
trackInstallation() {
// Track installation in analytics
console.log('App installed by user');
}
}
}
script>
Best Practices I've Learned Along the Way
After implementing PWAs for various clients , I've gathered some valuable insights:
Start with Lighthouse: Always use Lighthouse to audit your PWA before and after implementation. It's saved me countless debugging hours.
Optimize the Install Experience: I've found that showing the install prompt after users have engaged with your content (e.g., visited 2+ pages) increases installation rates by over 30%.
Test on Real Devices: Last year, I spent days debugging a PWA that worked perfectly on my development machine but failed on actual smartphones. Now I always test on at least 3 different devices with varying connection speeds.
Implement Offline Feedback: Let users know when they're offline and what features are available. I built a simple offline banner component that has become standard in all my PWAs:
// Check for online/offline status
window.addEventListener('online', updateOnlineStatus);
window.addEventListener('offline', updateOnlineStatus);
function updateOnlineStatus() {
const offlineBanner = document.getElementById('offline-banner');
if (navigator.onLine) {
offlineBanner.style.display = 'none';
} else {
offlineBanner.style.display = 'block';
}
}
// Call on initial load
document.addEventListener('DOMContentLoaded', updateOnlineStatus);
- Use Push Notifications Sparingly: In one project, excessive notifications drove users away. Now I follow a rule: only send notifications that provide immediate value.
Conclusion
Converting your website to a PWA isn't just following a technical checklist, it's about understanding your users' needs and creating an experience that works for them regardless of their network conditions or devices.
Here in Nigeria, where connectivity can be challenging and data costs are significant, PWAs have been game-changers for the businesses I work with. They've allowed my clients to reach users who otherwise might not be able to access their services reliably.
Whether you're working with a simple HTML site or a complex React application, the journey to PWA implementation follows similar principles. The key is to start small, test thoroughly, and continuously improve based on real user feedback.
Have you implemented a PWA for your website? What challenges did you face? I'd love to hear about your experiences in the comments!