Skip to main content

Progressive Web App (PWA)

Open Tarteel is a fully installable Progressive Web App with offline audio caching, providing a native app-like experience.

PWA Features

Installable

Add to home screen on mobile and desktop with custom icon and splash screen

Offline-First

Audio files cached for offline playback, service worker handles network failures

Fast Loading

Precached static assets, instant subsequent page loads

Native Experience

Fullscreen mode, standalone display, no browser UI

Service Worker Setup

Open Tarteel uses Serwist for service worker generation and management.

Next.js Configuration

import withSerwistInit from '@serwist/next';
import withBundleAnalyzer from '@next/bundle-analyzer';
import type { NextConfig } from 'next';

const isProduction = process.env.NODE_ENV === 'production';

const withSerwist = withSerwistInit({
  swSrc: 'src/sw.ts',                    // Service worker source
  swDest: 'public/sw.js',                // Output location
  cacheOnNavigation: true,               // Cache pages on navigation
  disable: !isProduction,                // Only enable in production
  register: isProduction,                // Auto-register in production
  maximumFileSizeToCacheInBytes: 100 * 1024 * 1024, // 100MB limit
});

const nextConfig: NextConfig = {
  reactStrictMode: !isProduction,
  transpilePackages: ['jotai-devtools'],
  compiler: {
    removeConsole: isProduction && { exclude: ['error'] },
  },
};

// Combine Serwist with bundle analyzer
export default withBundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
})(withSerwist(nextConfig));
The service worker is disabled in development to avoid caching issues during local development. It only activates in production builds.

Service Worker Implementation

import { defaultCache } from '@serwist/next/worker';
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist';
import {
  CacheableResponsePlugin,
  CacheFirst,
  ExpirationPlugin,
  Serwist,
} from 'serwist';

// TypeScript declarations
declare global {
  interface WorkerGlobalScope extends SerwistGlobalConfig {
    __SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
  }
}

declare const self: ServiceWorkerGlobalScope;

// Custom cache for MP3Quran audio files
const quranAudioCache = {
  matcher: ({ url }: { url: URL }) => {
    // Match MP3Quran audio URLs:
    // https://server12.mp3quran.net/001.mp3
    return (
      url.hostname.endsWith('.mp3quran.net') &&
      /^server\d+$/.test(url.hostname.split('.')[0]) &&
      /^\d+\.mp3$/.test(url.pathname.slice(1))
    );
  },
  handler: new CacheFirst({
    cacheName: 'quran-audio',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 20,                  // Cache up to 20 audio files
        maxAgeSeconds: 7 * 24 * 60 * 60, // Keep for 7 days
      }),
      new CacheableResponsePlugin({
        statuses: [200],                 // Only cache successful responses
      }),
    ],
  }),
};

// Prepend our custom cache before default caches
const runtimeCaching = [quranAudioCache, ...defaultCache];

// Initialize Serwist service worker
const serwist = new Serwist({
  precacheEntries: self.__SW_MANIFEST,   // Injected by build process
  skipWaiting: true,                     // Activate new SW immediately
  clientsClaim: true,                    // Take control of all clients
  navigationPreload: true,               // Speed up navigation requests
  runtimeCaching,
});

// Register event listeners
serwist.addEventListeners();

Cache Strategy Explained

Strategy: Cache First
  1. Check cache for audio file
  2. If found, return cached version instantly
  3. If not found, fetch from network
  4. Cache the response for future requests
// Example: User plays Surah Al-Fatihah
// URL: https://server12.mp3quran.net/001.mp3

// First play: Network fetch → Cache → Play
// Subsequent plays: Cache → Play (instant)
maxEntries
number
default:"20"
Maximum 20 audio files cached. LRU (Least Recently Used) eviction when limit exceeded.
maxAgeSeconds
number
default:"604800"
Cache duration: 7 days (604,800 seconds)

Web App Manifest

import { MetadataRoute } from 'next';

export default function manifest(): MetadataRoute.Manifest {
  return {
    id: '9a008c3173fcca3c4def71adedd5bd3f',
    theme_color: '#000000',
    background_color: '#000000',
    display: 'standalone',               // Hide browser UI
    scope: '/',
    start_url: '/',
    name: 'Open Tarteel',
    short_name: 'Open Tarteel',
    description: 'Quran streaming application',
    
    dir: 'auto',                         // RTL/LTR auto-detection
    lang: 'auto',
    orientation: 'portrait',
    
    // Launch behavior
    launch_handler: {
      client_mode: 'auto',
    },
    
    // Handle external links
    handle_links: [
      {
        url: 'https://tarteel.quran.us.kg',
        action: 'navigate',
      },
    ],
    
    categories: ['education', 'books', 'religion'],
    prefer_related_applications: false,
    
    // Web app identity
    web_apps: [
      {
        web_app_identity: 'https://tarteel.quran.us.kg/',
      },
    ],
    scope_extensions: [{ origin: 'tarteel.quran.us.kg' }],
    
    // App icons
    icons: [
      {
        src: 'images/192x192.png',
        sizes: '192x192',
        type: 'image/png',
      },
      {
        src: 'images/512x512.png',
        sizes: '512x512',
        type: 'image/png',
      },
    ],
    
    // Screenshots for app stores
    screenshots: [
      {
        src: '/screenshots/desktop.png',
        sizes: '1919x917',
        type: 'image/png',
        platform: 'wide',
        form_factor: 'wide',
        label: 'Open Tarteel - Desktop',
      },
      {
        src: '/screenshots/mobile.png',
        sizes: '1442x3202',
        type: 'image/png',
        platform: 'narrow',
        form_factor: 'narrow',
        label: 'Open Tarteel - Mobile',
      },
    ],
    
    // App shortcuts
    shortcuts: [
      {
        name: 'Contact us',
        url: '/contact',
        description: 'Contact us page',
        icons: [
          {
            src: 'images/shortcuts/contact.png',
            sizes: '96x96',
            type: 'image/png',
          },
        ],
      },
      {
        name: 'About us',
        url: '/about',
        description: 'About us page',
        icons: [
          {
            src: 'images/shortcuts/about.png',
            sizes: '96x96',
            type: 'image/png',
          },
        ],
      },
    ],
  };
}

Manifest Properties

standalone
string
App runs in its own window without browser UI (looks like native app)
fullscreen
string
Full screen mode (currently controlled via JavaScript)
minimal-ui
string
Minimal browser controls
browser
string
Regular browser tab
Required Sizes:
  • 192x192px: Minimum size for Android
  • 512x512px: High-resolution displays
Optional Sizes:
  • 144x144px: Windows tiles
  • 96x96px: App shortcuts
  • 72x72px, 48x48px: Older devices
public/images/
├── 192x192.png
├── 512x512.png
└── shortcuts/
    ├── contact.png
    └── about.png
Screenshots appear in:
  • Chrome Web Store
  • Microsoft Store
  • Installation prompts
Requirements:
  • Wide (desktop): 1280x720 minimum
  • Narrow (mobile): 320x640 minimum
  • PNG or JPEG format
  • Descriptive labels

Installation

Browser Install Prompts

1

User Visits Site

Service worker registers and caches assets
2

Engagement Heuristics Met

Browser shows install prompt after:
  • User visits twice
  • 5 minutes elapsed between visits
  • User engages with page (clicks, scrolls)
3

Install Prompt Appears

User can:
  • Click “Install” in browser menu
  • Use A2HS (Add to Home Screen) banner
  • Desktop: Install from address bar icon
4

App Installed

Opens as standalone app with:
  • Custom icon on home screen/dock
  • No browser chrome
  • Offline capabilities

Platform-Specific Installation

  1. Open in Chrome or Edge
  2. Tap menu → “Add to Home screen”
  3. Or tap install banner when it appears
  4. App appears in app drawer
Features:
  • WebAPK generated by browser
  • Appears in app settings
  • Can be uninstalled like native apps

Offline Functionality

What Works Offline

UI Navigation

  • Home page
  • About page
  • Contact page
  • Settings

Audio Playback

  • Previously played surahs (up to 20)
  • Cached within last 7 days
  • Full player controls

User Preferences

  • Favorite reciters
  • Volume settings
  • Playback speed
  • Language preference

Cached Reciters

  • Previously fetched reciter lists
  • Last selected reciter
  • Moshaf information

What Requires Network

New Content

  • Fetching new reciters
  • Loading unplayed surahs
  • Updated reciter lists

External Features

  • Contact form submission
  • Feedback sending
  • Analytics

Testing PWA

Chrome DevTools Audit

1

Open DevTools

Press F12 or right-click → Inspect
2

Navigate to Lighthouse

Click “Lighthouse” tab
3

Run PWA Audit

  • Select “Progressive Web App” category
  • Choose “Mobile” or “Desktop”
  • Click “Analyze page load”
4

Review Results

Check for:
  • Installable (manifest valid)
  • Service worker registered
  • HTTPS (required for PWA)
  • Mobile-friendly

Service Worker Debugging

  1. Open DevTools → Application tab
  2. Click “Service Workers” in sidebar
  3. View status: Installing / Activated / Redundant
  4. Test:
    • Update on reload
    • Bypass for network
    • Unregister

Build and Test

# Build production version (enables service worker)
npm run build

# Start production server
npm start

# Visit http://localhost:3000
# Service worker will register
# Play some audio to populate cache
# Toggle offline mode in DevTools
# Verify offline playback works

Best Practices

  • Limit maxEntries to prevent excessive storage usage
  • Set reasonable maxAgeSeconds for audio files
  • Clear old caches on service worker updates
new ExpirationPlugin({
  maxEntries: 20,  // ~200MB max (10MB per audio file)
  maxAgeSeconds: 7 * 24 * 60 * 60, // Weekly refresh
})
  • skipWaiting: true activates new SW immediately
  • Notify users of updates with toast/banner
  • Implement update check on page visibility
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js').then(reg => {
    reg.addEventListener('updatefound', () => {
      console.log('New version available!');
    });
  });
}
Service workers only work on HTTPS (or localhost):
  • Use Vercel/Netlify for automatic HTTPS
  • Or Cloudflare for custom domains
  • Development: localhost works without HTTPS
Check and request persistent storage:
if (navigator.storage && navigator.storage.persist) {
  const isPersisted = await navigator.storage.persist();
  console.log(`Storage persisted: ${isPersisted}`);
}
Open Tarteel’s PWA configuration provides a robust offline-first experience with intelligent audio caching for uninterrupted Quran listening.

Build docs developers (and LLMs) love