Progressive Web App (PWA)
Last updated
Last updated
{
"name": "Location Tracker", // Full app name (shown in app stores, install dialogs)
"short_name": "LocTracker", // Short name (shown under app icon on home screen)
"description": "Track and display...", // App description
"start_url": "/", // URL that opens when app launches
"scope": "/", // Which URLs the PWA controls
"display": "standalone", // How the app looks when opened (see below)
"orientation": "portrait-primary", // Lock screen orientation
"background_color": "#0f172a", // Splash screen background color
"theme_color": "#0ea5e9", // Status bar / title bar color
"orientation": "portrait-primary",
"icons": [ // App icons for different sizes
{
"src": "/icons/icon-192x192.svg",
"sizes": "192x192",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-512x512.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-maskable-512x512.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "maskable"
}
],
"categories": ["navigation", "utilities"],
"lang": "en",
"dir": "ltr"
}import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ServiceWorkerRegistration } from "./components/ServiceWorkerRegistration";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Location Tracker",
description: "Track and display your real-time location on an interactive map",
manifest: "/manifest.json",
appleWebApp: {
capable: true,
statusBarStyle: "black-translucent",
title: "Location Tracker",
},
formatDetection: {
telephone: false,
},
openGraph: {
type: "website",
title: "Location Tracker",
description: "Track and display your real-time location on an interactive map",
},
};
export const viewport: Viewport = {
themeColor: "#0ea5e9",
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
viewportFit: "cover",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<head>
<link rel="apple-touch-icon" href="/icons/icon-192x192.svg" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes" />
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<ServiceWorkerRegistration />
</body>
</html>
);
}βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BROWSER β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β β β β β β β
β β Your Web β ββββΊ β Service β ββββΊ β Network β β
β β App β β Worker β β (Internet) β β
β β β ββββ β (Proxy) β ββββ β β β
β β β β β β β β
β ββββββββββββββββ ββββββββ¬ββββββββ ββββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββ β
β β Cache β β
β β Storage β β
β ββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββconst CACHE_NAME = 'location-tracker-v1';
const STATIC_ASSETS = [
'/',
'/manifest.json',
'/icons/icon-192x192.svg',
'/icons/icon-512x512.svg',
];
// Install event - cache static assets
// When SW is first installed
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(STATIC_ASSETS);
})
);
self.skipWaiting();
});
// Activate event - clean up old caches
// When SW takes control
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
})
);
self.clients.claim();
});
// Fetch event - network first, falling back to cache
// When app makes ANY request
self.addEventListener('fetch', (event) => {
// Skip non-GET requests
if (event.request.method !== 'GET') return;
// Skip cross-origin requests (like map tiles)
if (!event.request.url.startsWith(self.location.origin)) {
return;
}
event.respondWith(
fetch(event.request)
.then((response) => {
// Clone the response before caching
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// Fallback to cache if network fails
return caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse;
}
// Return offline fallback for navigation requests
if (event.request.mode === 'navigate') {
return caches.match('/');
}
return new Response('Offline', { status: 503 });
});
})
);
});'use client';
import { useEffect } from 'react';
export function ServiceWorkerRegistration() {
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
console.log('Service Worker registered successfully:', registration.scope);
})
.catch((error) => {
console.log('Service Worker registration failed:', error);
});
}
}, []);
return null;
}