ITSkillsCenter
Blog

PWA : web app comme native

14 دقائق للقراءة

Ce que vous saurez faire à la fin

  1. Transformer un site web existant en Progressive Web App (PWA) installable sur Android, iOS, Windows et macOS sans passer par les stores.
  2. Créer un manifest.json complet et configurer un service worker avec Workbox pour le mode offline et le cache intelligent.
  3. Implémenter l’installation à l’écran d’accueil avec un bouton custom qui maximise le taux de conversion.
  4. Optimiser les Core Web Vitals d’une PWA pour Lighthouse score 95+ et chargement sous 2 secondes en 3G.
  5. Déployer une PWA de catalogue produits pour une boutique de Touba qui fonctionne hors ligne et coûte 60 000 FCFA en hébergement annuel.

Durée : 4h. Pré-requis : connaissance HTML/CSS/JS de base, Node.js 20 LTS, un site existant (HTML statique ou framework JS), un nom de domaine en HTTPS obligatoire (Let’s Encrypt gratuit), Chrome ou Edge à jour pour les tests Lighthouse, budget 0 à 50 000 FCFA selon hébergeur.

Étape 1 — Pourquoi une PWA en 2026

Pour une PME sénégalaise, une PWA cumule les avantages du web et du mobile : un seul code, déploiement instantané sans validation de store, mise à jour automatique, pas de commission de 30% sur les ventes. Une boutique en ligne PWA peut être installée par un client en 2 clics depuis Chrome mobile, apparaît sur l’écran d’accueil avec icône et splash screen comme une vraie app.

Pinterest, Twitter, Starbucks et Uber utilisent des PWA en production. Pinterest a vu son temps de session augmenter de 40% et le revenu publicitaire de 44% après migration. Pour une PME, le coût total de possession d’une PWA est 5 fois inférieur à celui d’une app native dual Android/iOS.

Étape 2 — Vérifier le HTTPS et le domaine

# Une PWA exige obligatoirement HTTPS (sauf localhost en dev)
# Vérifier que votre site a un certificat valide :
curl -I https://ma-boutique.sn
# Doit retourner 200 OK avec headers Strict-Transport-Security

# Si vous êtes sur Apache/Nginx sans HTTPS, installer Let's Encrypt :
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d ma-boutique.sn -d www.ma-boutique.sn

# Auto-renouvellement vérifié :
sudo systemctl status certbot.timer

# Si hébergement mutualisé chez Sonatel ou OVH, le HTTPS est inclus
# Vérifier que le panel propose "SSL gratuit Let's Encrypt"

# Test SSL Labs (qualité du certificat) :
# https://www.ssllabs.com/ssltest/analyze.html?d=ma-boutique.sn
# Viser note A ou A+

Sans HTTPS, le service worker ne s’enregistre pas, et le navigateur refuse d’afficher le bouton d’installation. C’est la première étape non négociable. Ne jamais déployer une PWA en HTTP, même en pré-production.

Étape 3 — Créer le manifest.json complet

// fichier : public/manifest.json (à la racine du site)

{
  "name": "Boutique Ma PME Dakar",
  "short_name": "Ma PME",
  "description": "Catalogue et commandes en ligne pour PME sénégalaise",
  "start_url": "/?source=pwa",
  "display": "standalone",
  "orientation": "portrait-primary",
  "background_color": "#FF6B00",
  "theme_color": "#FF6B00",
  "lang": "fr-SN",
  "dir": "ltr",
  "scope": "/",
  "categories": ["shopping", "business", "lifestyle"],
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-maskable-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "shortcuts": [
    {
      "name": "Mon panier",
      "url": "/panier",
      "icons": [{ "src": "/icons/cart.png", "sizes": "96x96" }]
    },
    {
      "name": "Nouveautés",
      "url": "/produits/nouveautes"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/home.png",
      "sizes": "1080x1920",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ]
}

Les icônes maskable permettent à Android d’adapter votre icône à toutes les formes (cercle, carré, squircle Pixel) sans la déformer. Générer une seule icône 512×512 avec contenu centré dans 80% du carré.

Étape 4 — Lier le manifest et meta tags PWA

<!-- Dans le <head> de toutes vos pages HTML -->

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

<!-- Couleur de la barre d'adresse (Android) -->
<meta name="theme-color" content="#FF6B00">

<!-- Compatibilité iOS Safari (limité mais fonctionne) -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Ma PME">
<link rel="apple-touch-icon" href="/icons/icon-192.png">

<!-- Splash screens iOS (à générer pour chaque taille d'écran) -->
<link rel="apple-touch-startup-image" href="/splash/iphone-pro.png"
      media="(device-width: 430px) and (device-height: 932px)">

<!-- Compatibilité Microsoft (Windows tiles) -->
<meta name="msapplication-TileColor" content="#FF6B00">
<meta name="msapplication-TileImage" content="/icons/icon-144.png">

<!-- Description SEO et Open Graph -->
<meta name="description" content="Boutique en ligne de PME sénégalaise">
<meta property="og:image" content="https://ma-boutique.sn/og-image.png">

iOS Safari supporte les PWA depuis 16.4 mais avec limitations : pas de notifications push avant iOS 16.4, pas de Background Sync, support partiel du manifest. Pour un public iPhone majoritaire, prévoir une app native en plus.

Étape 5 — Service worker minimaliste (vanilla JS)

// fichier : sw.js à la racine du site

const CACHE_VERSION = 'v1.2.0';
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const RUNTIME_CACHE = `runtime-${CACHE_VERSION}`;

const ASSETS_TO_CACHE = [
  '/',
  '/index.html',
  '/styles/main.css',
  '/scripts/app.js',
  '/icons/icon-192.png',
  '/offline.html',
];

// Installation : pré-cacher les assets critiques
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(STATIC_CACHE)
      .then((cache) => cache.addAll(ASSETS_TO_CACHE))
      .then(() => self.skipWaiting())
  );
});

// Activation : nettoyer les anciens caches
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.filter((k) => ![STATIC_CACHE, RUNTIME_CACHE].includes(k))
            .map((k) => caches.delete(k))
      )
    ).then(() => self.clients.claim())
  );
});

// Stratégie network-first pour les pages HTML
self.addEventListener('fetch', (event) => {
  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request)
        .then((res) => {
          const cloned = res.clone();
          caches.open(RUNTIME_CACHE).then((c) => c.put(event.request, cloned));
          return res;
        })
        .catch(() => caches.match(event.request)
          .then((cached) => cached || caches.match('/offline.html')))
    );
  }
});

Un service worker mal écrit casse le site en production sans rollback possible (le SW reste en cache 24h). Toujours versionner les caches et tester abondamment en mode incognito avant déploiement.

Étape 6 — Enregistrer le service worker

// fichier : scripts/app.js

if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js', {
        scope: '/',
      });
      console.log('SW enregistré', registration.scope);

      // Détecter une mise à jour disponible :
      registration.addEventListener('updatefound', () => {
        const newWorker = registration.installing;
        newWorker.addEventListener('statechange', () => {
          if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
            // Afficher une notif "Mise à jour disponible"
            showUpdateBanner();
          }
        });
      });
    } catch (e) {
      console.error('Echec SW', e);
    }
  });
}

function showUpdateBanner() {
  const banner = document.createElement('div');
  banner.className = 'update-banner';
  banner.innerHTML = 'Nouvelle version disponible <button id="reload">Recharger</button>';
  document.body.appendChild(banner);
  document.getElementById('reload').addEventListener('click', () => {
    window.location.reload();
  });
}

Sans gestion des updates, vos utilisateurs gardent l’ancienne version pendant des semaines. Un mécanisme de notification visuel + bouton « Recharger » augmente le taux d’adoption des nouvelles versions de 12% à 78%.

Étape 7 — Workbox pour des stratégies de cache avancées

# Workbox simplifie le SW avec des stratégies pré-construites

# Installer via npm :
npm install workbox-cli --save-dev

# Générer un SW automatiquement :
npx workbox wizard
# Suivre l'assistant : sélectionner le dossier dist/, choisir generateSW

# Ou écrire un workbox-config.js custom :
module.exports = {
  globDirectory: 'dist/',
  globPatterns: ['**/*.{html,js,css,png,jpg,svg,woff2}'],
  swDest: 'dist/sw.js',
  runtimeCaching: [
    {
      urlPattern: /^https:\/\/api\.ma-boutique\.sn\//,
      handler: 'NetworkFirst',
      options: {
        cacheName: 'api-cache',
        expiration: { maxEntries: 50, maxAgeSeconds: 5 * 60 },
      },
    },
    {
      urlPattern: /\.(?:png|jpg|jpeg|svg|webp)$/,
      handler: 'CacheFirst',
      options: {
        cacheName: 'images',
        expiration: { maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60 },
      },
    },
  ],
};

# Builder :
npx workbox generateSW workbox-config.js

Workbox gère 5 stratégies clés : CacheFirst (images, polices), NetworkFirst (HTML, API dynamiques), StaleWhileRevalidate (CSS/JS), NetworkOnly et CacheOnly. Bien choisir la stratégie par type de ressource est ce qui fait la différence entre une PWA mauvaise et excellente.

Étape 8 — Bouton d’installation custom

// scripts/install-button.js

let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  showInstallButton();
});

function showInstallButton() {
  const btn = document.getElementById('install-pwa');
  if (!btn) return;

  btn.style.display = 'flex';
  btn.addEventListener('click', async () => {
    if (!deferredPrompt) return;
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    console.log(`Choix utilisateur : ${outcome}`);
    if (outcome === 'accepted') {
      gtag('event', 'pwa_installed'); // tracking analytics
    }
    deferredPrompt = null;
    btn.style.display = 'none';
  });
}

window.addEventListener('appinstalled', () => {
  console.log('PWA installée');
  // Cacher tous les CTAs d'install
});

// HTML correspondant :
// <button id="install-pwa" style="display:none">
//   📱 Installer l'app
// </button>

Le prompt natif d’install est moche et ignoré par 90% des utilisateurs. Un bouton custom avec icône, texte clair et placement stratégique (sticky bar, banner) triple le taux d’installation. Apparaît seulement si la PWA est éligible.

Étape 9 — Page offline élégante

<!-- offline.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <title>Hors ligne - Ma PME</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body {
      font-family: system-ui, -apple-system, sans-serif;
      display: flex; align-items: center; justify-content: center;
      min-height: 100vh; margin: 0; padding: 20px;
      background: linear-gradient(135deg, #FF6B00, #FF9A3C);
      color: white; text-align: center;
    }
    .container { max-width: 400px; }
    .icon { font-size: 80px; margin-bottom: 20px; }
    h1 { font-size: 28px; margin: 0 0 10px; }
    p { opacity: 0.9; line-height: 1.5; }
    button {
      background: white; color: #FF6B00; border: none;
      padding: 12px 30px; border-radius: 25px; font-weight: 600;
      margin-top: 20px; cursor: pointer; font-size: 16px;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="icon">📡</div>
    <h1>Pas de connexion</h1>
    <p>Vérifiez votre data ou wifi. Vos derniers contenus restent accessibles.</p>
    <button onclick="location.reload()">Réessayer</button>
  </div>
</body>
</html>

Sans page offline, l’utilisateur voit le dinosaure Chrome. Une page offline brandée transforme l’incident technique en moment de marque : le client se rappelle que l’app fonctionne malgré tout.

Étape 10 — Tests Lighthouse et PWA Builder

# Test Lighthouse depuis Chrome DevTools :
# F12 > onglet Lighthouse > cocher "Progressive Web App"
# Cliquer "Analyze page load"
# Viser score 100 sur la catégorie PWA

# Test depuis CLI :
npm install -g lighthouse
lighthouse https://ma-boutique.sn --view --preset=desktop
lighthouse https://ma-boutique.sn --view --preset=mobile

# Outil Microsoft PWA Builder (très complet) :
# https://www.pwabuilder.com/
# Saisir l'URL, obtenir un rapport détaillé + packages Android/iOS prêts

# Vérifier le manifest :
# Chrome DevTools > Application > Manifest
# Vérifier qu'il n'y a aucune erreur

# Vérifier le service worker :
# Chrome DevTools > Application > Service Workers
# Status doit être "activated and is running"

PWA Builder de Microsoft est une mine d’or : il génère automatiquement un APK Android signé pour Play Store (via Trusted Web Activity) et un package iOS pour App Store. Vous pouvez ainsi avoir votre PWA sur les 2 stores sans coder une seule ligne native.

Étape 11 — Notifications push web (Android et desktop)

// scripts/push.js

async function subscribeToPush() {
  if (!('Notification' in window) || !('serviceWorker' in navigator)) return;

  const permission = await Notification.requestPermission();
  if (permission !== 'granted') return;

  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array('VOTRE_VAPID_PUBLIC_KEY'),
  });

  // Envoyer la subscription au backend
  await fetch('/api/push/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription),
  });
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const rawData = window.atob(base64);
  return Uint8Array.from([...rawData].map(c => c.charCodeAt(0)));
}

// Dans sw.js, écouter les push :
self.addEventListener('push', (event) => {
  const data = event.data.json();
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icons/icon-192.png',
      badge: '/icons/badge.png',
      vibrate: [200, 100, 200],
    })
  );
});

Les notifications push web fonctionnent sur Chrome Android, Firefox, Edge, et iOS Safari 16.4+ (uniquement si l’utilisateur a installé la PWA à l’écran d’accueil). Quotient de réception : 78% sur Android, 31% sur iOS.

Étape 12 — Hébergement performant pour PWA

Options recommandées pour une PWA en Afrique :

1. Cloudflare Pages (gratuit, CDN mondial)
   - Build automatique depuis Git
   - Edge cache à Lagos, Le Cap
   - SSL inclus, custom domain gratuit
   - Limites : 500 builds/mois, 100 sites

2. Vercel (gratuit pour usage perso)
   - Optimisé Next.js mais marche pour tout
   - Edge functions pour APIs
   - Analytics gratuit
   - Limites : 100 Go bande passante/mois

3. Firebase Hosting (gratuit jusqu'à 10 Go)
   - Déploiement en 1 commande : firebase deploy
   - HTTPS, CDN inclus
   - Intégration Firebase Auth/Firestore
   - Hosting gratuit jusqu'à 360 Mo de transfert/jour

4. VPS Hetzner Falkenstein (4 EUR/mois)
   - Performance maximale si hébergé en Europe
   - TTFB depuis Dakar : 90 ms
   - Total annuel : 32 000 FCFA

5. OVH chez vous (Dakar local)
   - TTFB ultra-bas pour utilisateurs sénégalais
   - Plus cher : ~80 000 FCFA/an
   - Pas de CDN mondial inclus

Pour 90% des PME, Cloudflare Pages ou Firebase Hosting suffisent largement et coûtent 0 FCFA. Le CDN mondial fait que vos pages chargent vite à Lagos, Bamako ou Abidjan, pas seulement à Dakar.

Étape 13 — Mesurer l’engagement post-installation

// Tracker les events PWA dans Google Analytics 4

// Détection display-mode standalone (utilisateur dans l'app) :
window.addEventListener('DOMContentLoaded', () => {
  const isStandalone = window.matchMedia('(display-mode: standalone)').matches
    || window.navigator.standalone === true;

  if (isStandalone) {
    gtag('event', 'pwa_session', {
      app_version: '1.2.0',
      install_source: new URLSearchParams(location.search).get('source'),
    });
  }
});

// Détecter l'événement install :
window.addEventListener('appinstalled', () => {
  gtag('event', 'pwa_install_completed');
});

// Détecter le rejet du prompt :
window.addEventListener('beforeinstallprompt', (e) => {
  gtag('event', 'pwa_install_prompt_shown');
});

// KPIs à suivre dans GA4 :
// - Nombre d'installations PWA / total visiteurs
// - Sessions standalone vs sessions browser
// - Durée moyenne d'une session standalone
// - Taux de retour sur 7 jours pour utilisateurs PWA
// - Conversion (achat) standalone vs browser

Une PWA bien faite multiplie par 2,5 le temps de session vs site mobile classique. Pour une boutique e-commerce, le taux de conversion PWA standalone est en moyenne 3x supérieur au navigateur. Mesurez pour prouver le ROI à la direction.

Étape 14 — Publier sur Play Store via TWA

# Trusted Web Activity (TWA) emballe une PWA dans un APK Android natif
# Avantage : visibilité Play Store + reviews + analytics Google Play

# Outil : Bubblewrap (officiel Google)
npm install -g @bubblewrap/cli

# Initialiser le projet TWA :
bubblewrap init --manifest https://ma-boutique.sn/manifest.json

# Bubblewrap demande :
# - Package ID : com.mapme.boutique
# - Domain : ma-boutique.sn
# - Signing key : nouvelle ou existante

# Générer l'APK :
bubblewrap build

# Génère :
# - app-release-bundle.aab pour Play Store
# - app-release-signed.apk pour test direct
# - assetlinks.json à uploader sur https://ma-boutique.sn/.well-known/

# Vérifier que le Digital Asset Links est accessible :
curl https://ma-boutique.sn/.well-known/assetlinks.json
# Crucial pour que Chrome ne montre pas la barre d'URL dans la TWA

Une PWA publiée en TWA sur Play Store coûte 25 USD une seule fois (frais Google) et donne accès à 3,5 milliards d’utilisateurs Android. Toutes les mises à jour de votre site web sont reflétées instantanément sans nouvelle soumission au store.

Erreurs classiques à éviter

  • Cacher le HTML index.html en CacheFirst : les utilisateurs ne verront jamais vos nouvelles pages. Toujours NetworkFirst ou StaleWhileRevalidate pour le HTML.
  • Oublier de mettre à jour CACHE_VERSION : les nouveaux assets ne sont pas téléchargés. Incrémenter la version à chaque déploiement.
  • Demander la permission notif au premier chargement : 80% des utilisateurs refusent par réflexe. Demander après 2 visites ou après une action positive (ajout panier).
  • Ignorer les icônes maskable : sur Pixel et Samsung, votre icône apparaît dans un cercle blanc moche. Toujours fournir une variante maskable.
  • Tester uniquement sur Chrome desktop : Safari iOS a 30% de comportements différents. Toujours tester sur Safari iPhone réel et Chrome Android Tecno.
  • Bloquer le service worker en dev : oublier d’unregister le SW casse les modifs en local pendant des heures. Utiliser le mode incognito ou « Update on reload » dans DevTools.

Checklist mise en production PWA

✓ HTTPS valide avec note SSL Labs A ou A+
✓ manifest.json complet avec icônes 192, 512 et maskable
✓ Meta theme-color, apple-touch-icon dans le head
✓ Service worker enregistré et activé sans erreur
✓ Stratégies de cache adaptées par type de ressource (Workbox)
✓ Page offline brandée avec bouton réessayer
✓ Bouton d'installation custom (beforeinstallprompt)
✓ Détection des updates avec banner "Recharger"
✓ Lighthouse score 95+ en mode mobile
✓ Tests sur Chrome Android, Safari iOS 16.4+, Edge Windows
✓ Notifications push configurées (VAPID, backend abonnements)
✓ Hébergement CDN avec edge africain (Cloudflare Pages, Vercel)
✓ Tracking GA4 events install et display-mode standalone
✓ Document assetlinks.json publié si TWA Play Store
✓ APK TWA généré et publié sur Play Store
Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité