Programování

PWA vlastními silami: manifest a service worker bez frameworku

Jak z běžného webu udělat instalovatelnou PWA aplikaci s offline režimem: web app manifest, service worker, strategie cache, aktualizace a ladění.

12. 10. 2026 · 5 min čtení

Progresivní webová aplikace (PWA) je web, který si uživatel může nainstalovat na plochu telefonu nebo počítače, spouští se v samostatném okně a funguje i offline. Nepotřebujete k tomu framework ani obchod s aplikacemi – stačí dva soubory navíc: manifest a service worker. Podmínkou je HTTPS (výjimkou je localhost při vývoji).

1. Web App Manifest

{
  "name": "Moje poznámky",
  "short_name": "Poznámky",
  "description": "Jednoduché poznámky, které fungují i offline.",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2563eb",
  "lang": "cs",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}

Uložte jako /manifest.webmanifest a připojte v HTML:

<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#2563eb">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
  • display: standalone – aplikace běží bez adresního řádku.
  • short_name – zobrazí se pod ikonou, držte ho krátký.
  • Maskovatelná ikona (purpose: maskable) má důležitý obsah uvnitř bezpečné zóny (středový kruh o průměru 80 % rozměru), protože Android ji ořízne do kruhu nebo jiného tvaru. Nedávejte "any maskable" do jedné ikony – v jednom z režimů by vypadala špatně.
  • iOS používá pro ikonu na ploše apple-touch-icon (180 × 180 px).

2. Registrace service workeru

// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const reg = await navigator.serviceWorker.register('/sw.js');
      console.log('SW zaregistrován, scope:', reg.scope);
    } catch (err) {
      console.error('Registrace SW selhala:', err);
    }
  });
}

Service worker umístěte do kořene webu. Jeho výchozí scope je složka, ve které leží – /js/sw.js by kontroloval jen URL pod /js/.

3. Service worker s offline režimem

// sw.js
const VERSION = 'v3';
const STATIC_CACHE = `static-${VERSION}`;
const PRECACHE = [
  '/',
  '/offline.html',
  '/style.css',
  '/main.js',
  '/icons/icon-192.png',
];

// instalace: stáhnout základní soubory
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(STATIC_CACHE).then(cache => cache.addAll(PRECACHE))
  );
});

// aktivace: smazat staré verze cache
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== STATIC_CACHE).map(k => caches.delete(k)))
    ).then(() => self.clients.claim())
  );
});

self.addEventListener('fetch', event => {
  const { request } = event;
  if (request.method !== 'GET') return;

  const url = new URL(request.url);
  if (url.origin !== location.origin) return;   // cizí domény nechat být

  // HTML stránky: nejdřív síť, při výpadku cache, nakonec offline stránka
  if (request.mode === 'navigate') {
    event.respondWith(
      fetch(request)
        .then(res => {
          const copy = res.clone();
          caches.open(STATIC_CACHE).then(c => c.put(request, copy));
          return res;
        })
        .catch(async () =>
          (await caches.match(request)) ?? caches.match('/offline.html')
        )
    );
    return;
  }

  // ostatní (CSS, JS, obrázky): nejdřív cache, jinak síť
  event.respondWith(
    caches.match(request).then(cached => cached ?? fetch(request))
  );
});

event.waitUntil drží service worker naživu, dokud Promise neskončí. res.clone() je nutný, protože tělo odpovědi lze přečíst jen jednou – jednu kopii uložíme, druhou vrátíme stránce.

Strategie cache – kterou kdy

Strategie Vhodné pro
Cache first assety s hashem v názvu, fonty, ikony
Network first HTML, data, která mají být aktuální
Stale-while-revalidate avatary, méně kritická data – vrátí cache a na pozadí stáhne novou verzi
Network only POST požadavky, platby, přihlášení

Necachujte odpovědi API s osobními daty bez rozmyslu – zůstanou v zařízení i po odhlášení, pokud cache explicitně nesmažete.

4. Aktualizace: největší past

Prohlížeč nový sw.js stáhne, když se liší alespoň o bajt. Nový worker se ale neaktivuje, dokud jsou otevřené stránky ovládané tím starým. Uživatel tak může dlouho vidět starou verzi.

Proto při každé změně cachovaných souborů zvyšte VERSION a nabídněte uživateli aktualizaci:

// sw.js
self.addEventListener('message', event => {
  if (event.data === 'SKIP_WAITING') self.skipWaiting();
});
// main.js
const reg = await navigator.serviceWorker.register('/sw.js');
reg.addEventListener('updatefound', () => {
  const worker = reg.installing;
  worker.addEventListener('statechange', () => {
    if (worker.state === 'installed' && navigator.serviceWorker.controller) {
      if (confirm('Je k dispozici nová verze. Načíst?')) worker.postMessage('SKIP_WAITING');
    }
  });
});

let reloaded = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (!reloaded) { reloaded = true; location.reload(); }
});

Samotný sw.js servírujte s hlavičkou Cache-Control: no-cache, aby ho nedržela HTTP cache. Nginx:

location = /sw.js {
    add_header Cache-Control "no-cache";
}

5. Vlastní tlačítko „Nainstalovat”

Prohlížeče na bázi Chromia vyvolají událost beforeinstallprompt:

let deferredPrompt;
const btn = document.getElementById('install');

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

btn.addEventListener('click', async () => {
  deferredPrompt.prompt();
  await deferredPrompt.userChoice;
  deferredPrompt = null;
  btn.hidden = true;
});

Safari tuto událost nepodporuje – na iPhonu se instaluje přes Sdílet → Přidat na plochu. Pro iOS je dobré zobrazit krátkou nápovědu.

6. Ladění

V Chrome DevTools → Application:

  • Manifest – kontrola chyb, náhled ikon a informace, zda je aplikace instalovatelná.
  • Service workers – stav, tlačítko Update, volba Update on reload (během vývoje ji zapněte) a Offline pro test bez sítě.
  • Cache storage – co přesně je v cache.
  • Storage → Clear site data – čistý start, když se zamotáte.

Lighthouse už samostatnou kategorii PWA nemá (byla odstraněna v roce 2024), instalovatelnost tedy ověřujte v panelu Manifest.

Pokud nechcete psát service worker ručně, nástroj Workbox (od Googlu) nebo plugin vite-plugin-pwa vygenerují precache seznam při buildu automaticky. Psát ho jednou ručně se ale vyplatí – pak víte, co dělá.