DEV Community

Tianya School
Tianya School

Posted on

Web App Manifest Creating an Immersive PWA Experience

Let’s explore the Web App Manifest, a key tool for building Progressive Web Apps (PWAs). PWAs make web apps feel like native applications, supporting offline access, installation to the home screen, and push notifications. The Web App Manifest is the core file that defines a PWA’s “identity,” telling browsers its name, icon, display mode, and supported features.

What is the Web App Manifest?

The Web App Manifest is a JSON file, typically named manifest.json, that defines a PWA’s metadata and behavior. It informs the browser that your web app is an “application,” enabling it to be added to the home screen, run in full-screen mode, or display a custom splash screen. Key features include:

  • App Metadata: Name, icons, and description for home screen display.
  • Display Mode: Controls whether it behaves like a browser tab (browser) or a full-screen app (standalone).
  • Theme and Styling: Sets theme and background colors for an immersive experience.
  • Feature Support: Defines offline behavior, screen orientation, and shortcuts.

The Manifest acts as the PWA’s “ID card,” working with Service Workers (for offline and caching) and HTTPS to deliver a native-like experience. We’ll start with basic configuration and progress to advanced scenarios.


Environment Setup

To work with the Web App Manifest, we’ll use a React project (via Create React App) with Service Worker and HTTPS support. You’ll need Node.js (18.x+).

Create a project:

npx create-react-app pwa-demo
cd pwa-demo
Enter fullscreen mode Exit fullscreen mode

Install tools:

npm install workbox-cli
Enter fullscreen mode Exit fullscreen mode

Directory structure:

pwa-demo/
├── public/
│   ├── index.html
│   ├── manifest.json
├── src/
│   ├── index.js
│   ├── App.js
│   ├── service-worker.js
├── package.json
Enter fullscreen mode Exit fullscreen mode

Run npm start and visit localhost:3000. The default React project includes PWA basics, and we’ll add the Manifest and Service Worker.


Basic Manifest Configuration

Place the Manifest file in public/manifest.json. Start with a simple version:

{
  "name": "PWA Demo",
  "short_name": "Demo",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Prepare icons (192x192 and 512x512 PNGs) and place them in public/. Update public/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#007bff" />
  <link rel="manifest" href="/manifest.json" />
  <link rel="icon" href="/favicon.ico" />
  <title>PWA Demo</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Run npm start and check Chrome DevTools (Application > Manifest). The Manifest loads, showing:

  • App Name: PWA Demo
  • Short Name: Demo
  • Start URL: /
  • Display Mode: standalone (full-screen, no browser UI)
  • Theme/Background Color: Blue/White
  • Icons: 192x192 and 512x512

On Chrome (Android or desktop), click “Add to Home Screen” to add the app icon, which launches like a native app.

Field Breakdown

  • name: Full app name, shown on the home screen or install prompt.
  • short_name: Short name for limited space (e.g., notifications).
  • start_url: URL launched when the app starts, / points to the homepage.
  • display: Display mode:
    • fullscreen: Full-screen, no status bar.
    • standalone: Native app-like, no browser UI.
    • minimal-ui: Minimal browser UI.
    • browser: Standard browser tab.
  • background_color: Background color during startup (before loading).
  • theme_color: Browser address bar/status bar color.
  • icons: Array of icons with paths, sizes, and types.

Service Worker: Offline Support

The Manifest enables installation, while Service Workers provide offline functionality. Create React App includes Service Worker setup, but it needs activation.

Update src/index.js:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import * as serviceWorkerRegistration from './serviceWorkerRegistration';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

serviceWorkerRegistration.register();
Enter fullscreen mode Exit fullscreen mode

The default src/serviceWorkerRegistration.js handles registration (omitted for brevity). Run npm start; the Service Worker registers but activates only in production. Build and test:

npm run build
npx serve -s build
Enter fullscreen mode Exit fullscreen mode

Visit localhost:5000. DevTools (Application > Service Workers) shows the Service Worker running. Disconnect the network, and the page still loads (caching index.html and JS).

Custom Service Worker

Use Workbox for custom caching. Create src/service-worker.js:

import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';

precacheAndRoute(self.__WB_MANIFEST);

registerRoute(
  ({ request }) => request.destination === 'image',
  new StaleWhileRevalidate({
    cacheName: 'images'
  })
);

self.addEventListener('install', event => {
  console.log('Service Worker installed');
});

self.addEventListener('activate', event => {
  console.log('Service Worker activated');
});
Enter fullscreen mode Exit fullscreen mode

Update src/serviceWorkerRegistration.js to use the custom Service Worker:

export function register() {
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/service-worker.js').then(registration => {
        console.log('Service Worker registered:', registration);
      });
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Update package.json for Workbox build:

{
  "scripts": {
    "build": "react-scripts build && workbox generateSW workbox-config.js",
    "start": "react-scripts start"
  }
}
Enter fullscreen mode Exit fullscreen mode

Create workbox-config.js:

module.exports = {
  globDirectory: 'build/',
  globPatterns: ['**/*.{html,js,css,png,jpg}'],
  swDest: 'build/service-worker.js',
  clientsClaim: true,
  skipWaiting: true
};
Enter fullscreen mode Exit fullscreen mode

Run npm run build && npx serve -s build. Images and static assets are cached, accessible offline.


Extending the Manifest: Additional Features

Enhance manifest.json with more features:

{
  "name": "PWA Demo",
  "short_name": "Demo",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "orientation": "portrait",
  "scope": "/",
  "related_applications": [
    {
      "platform": "play",
      "url": "https://play.google.com/store/apps/details?id=com.example.app"
    }
  ],
  "shortcuts": [
    {
      "name": "View Profile",
      "url": "/profile",
      "icons": [
        {
          "src": "/icon-profile-96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode
  • orientation: Locks screen orientation (portrait or landscape).
  • scope: Restricts PWA navigation, e.g., /profile opens within the PWA.
  • related_applications: Links to native apps (e.g., Google Play).
  • shortcuts: Home screen shortcuts for quick access to /profile.

Prepare icon-profile-96.png and place it in public/. Test: After installing the PWA, long-press the icon to see the “View Profile” shortcut.


Dynamic Manifest

Dynamically generate the Manifest based on user state. Update public/index.html:

<link rel="manifest" href="/manifest.json" id="manifest" />
Enter fullscreen mode Exit fullscreen mode

Update src/App.js:

import { useEffect } from 'react';

function App() {
  useEffect(() => {
    const user = { name: 'Alice' };
    const manifest = {
      name: `PWA Demo for ${user.name}`,
      short_name: 'Demo',
      start_url: '/',
      display: 'standalone',
      background_color: '#ffffff',
      theme_color: '#007bff',
      icons: [
        {
          src: '/icon-192.png',
          sizes: '192x192',
          type: 'image/png'
        },
        {
          src: '/icon-512.png',
          sizes: '512x512',
          type: 'image/png'
        }
      ]
    };

    const stringManifest = JSON.stringify(manifest);
    const blob = new Blob([stringManifest], { type: 'application/json' });
    const manifestURL = URL.createObjectURL(blob);
    document.getElementById('manifest').setAttribute('href', manifestURL);
  }, []);

  return (
    <div style={{ padding: 20 }}>
      <h1>Welcome to PWA Demo</h1>
      <p>Add this to your home screen!</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Install the PWA; the app name becomes “PWA Demo for Alice”. The dynamic Manifest adjusts based on user data.


Push Notifications

PWAs support push notifications using Service Workers and the Web Push API. Install web-push:

npm install web-push
Enter fullscreen mode Exit fullscreen mode

Update src/service-worker.js:

import { precacheAndRoute } from 'workbox-precaching';

precacheAndRoute(self.__WB_MANIFEST);

self.addEventListener('push', event => {
  const data = event.data.json();
  self.registration.showNotification(data.title, {
    body: data.body,
    icon: '/icon-192.png'
  });
});
Enter fullscreen mode Exit fullscreen mode

Backend (Node.js) to send push notifications:

const webpush = require('web-push');

const vapidKeys = {
  publicKey: 'YOUR_PUBLIC_KEY',
  privateKey: 'YOUR_PRIVATE_KEY'
};

webpush.setVapidDetails(
  'mailto:example@yourdomain.com',
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

const pushSubscription = {
  endpoint: 'USER_ENDPOINT',
  keys: {
    auth: 'USER_AUTH',
    p256dh: 'USER_P256DH'
  }
};

webpush.sendNotification(pushSubscription, JSON.stringify({
  title: 'New Message',
  body: 'Check out the latest updates!'
}));
Enter fullscreen mode Exit fullscreen mode

Frontend subscription:

src/App.js:

import { useEffect } from 'react';

function App() {
  useEffect(() => {
    async function subscribePush() {
      const registration = await navigator.serviceWorker.ready;
      const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: 'YOUR_PUBLIC_KEY'
      });
      console.log('Push subscribed:', subscription);
      // Send subscription to backend
    }

    if ('serviceWorker' in navigator && 'PushManager' in window) {
      subscribePush();
    }
  }, []);

  return (
    <div style={{ padding: 20 }}>
      <h1>Welcome to PWA Demo</h1>
      <p>Add this to your home screen!</p>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Generate VAPID keys:

npx web-push generate-vapid-keys
Enter fullscreen mode Exit fullscreen mode

Run the backend script to send notifications. Users must grant notification permissions.


Using in Next.js

Implement a PWA with Next.js (App Router). Create a project:

npx create-next-app@latest next-pwa
cd next-pwa
Enter fullscreen mode Exit fullscreen mode

Create public/manifest.json:

{
  "name": "Next PWA",
  "short_name": "Next",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Update app/layout.tsx:

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <meta name="theme-color" content="#007bff" />
        <link rel="manifest" href="/manifest.json" />
      </head>
      <body>{children}</body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Update app/page.tsx:

export default function Home() {
  return (
    <div style={{ padding: 20 }}>
      <h1>Next.js PWA</h1>
      <p>Add to home screen!</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Add Service Worker with next-pwa:

npm install next-pwa
Enter fullscreen mode Exit fullscreen mode

Update next.config.js:

const withPWA = require('next-pwa')({
  dest: 'public',
  register: true,
  skipWaiting: true
});

module.exports = withPWA({
  // Next.js config
});
Enter fullscreen mode Exit fullscreen mode

Run npm run build && npm start. The PWA is installable and works offline.


Real-World Scenario: E-commerce PWA

Create an e-commerce PWA with product list, details, and cart.

public/manifest.json:

{
  "name": "Ecommerce PWA",
  "short_name": "Shop",
  "start_url": "/products",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "shortcuts": [
    {
      "name": "Cart",
      "url": "/cart",
      "icons": [
        {
          "src": "/icon-cart-96.png",
          "sizes": "96x96",
          "type": "image/png"
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

app/products/page.tsx:

import Image from 'next/image';

export default async function Products() {
  const data = await fetch('https://jsonplaceholder.typicode.com/posts').then(res => res.json());

  return (
    <div style={{ padding: 20 }}>
      <h1>Products</h1>
      <Image
        src="https://via.placeholder.com/500"
        alt="Banner"
        width={500}
        height={200}
      />
      <ul>
        {data.slice(0, 5).map(post => (
          <li key={post.id}>
            <a href={`/products/${post.id}`}>{post.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

app/products/[id]/page.tsx:

import Image from 'next/image';

export default async function ProductPage({ params }) {
  const data = await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`).then(res => res.json());

  return (
    <div style={{ padding: 20 }}>
      <h1>{data.title}</h1>
      <Image
        src="https://via.placeholder.com/300"
        alt="Product"
        width={300}
        height={300}
      />
      <p>{data.body}</p>
      <button onClick={() => alert('Added to cart!')}>Add to Cart</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

app/cart/page.tsx:

export default function Cart() {
  return (
    <div style={{ padding: 20 }}>
      <h1>Cart</h1>
      <p>Your cart is empty.</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Run npm run dev and visit /products. The PWA is installable, and long-pressing the icon shows a “Cart” shortcut. Service Worker caches resources for offline access.


Performance Testing

Test PWA performance with Lighthouse:

npx lighthouse http://localhost:3000 --output json --output-path pwa-report.json
Enter fullscreen mode Exit fullscreen mode

Report results:

  • FCP: ~700ms
  • LCP: ~900ms
  • TTI: ~1s
  • PWA Score: High (complete Manifest and Service Worker)

Conclusion (Technical Details)

The Web App Manifest is central to PWAs, defining app metadata and behavior. The examples demonstrated:

  • Basic Manifest setup (name, icons, display mode).
  • Service Worker for offline support.
  • Dynamic Manifest and push notifications.
  • Next.js PWA integration.
  • E-commerce PWA scenario (products, cart, shortcuts).

Run these examples, install the PWA on your phone, and test offline access and notifications to experience the immersive PWA feel!

Top comments (0)