Use Cases (Actualizado: 2/6/2026)

Crear una PWA con Claude Code: manifest, Service Worker y offline

Guia practica para crear una PWA con Claude Code: manifest, iconos, Service Worker, fallback offline, cache y validacion.

Crear una PWA con Claude Code: manifest, Service Worker y offline

Una PWA, o Progressive Web App, es una aplicacion web que puede comportarse mas como una aplicacion instalada: tiene nombre e icono, puede abrirse en una ventana independiente, puede guardar recursos en cache y puede mostrar una pantalla util cuando la conexion falla.

El error habitual es pensar que basta con anadir un manifest.webmanifest. Una PWA real tambien necesita iconos correctos, registro de Service Worker, una pagina offline, una estrategia de cache, pruebas de instalabilidad y validacion en Chrome DevTools y Lighthouse. Un solo icono con ruta 404 o un HTML viejo en cache puede romper la experiencia despues de desplegar.

En esta guia vamos a implementar una PWA con Claude Code de forma segura y entendible. Si todavia no tienes claro como trabajar con la herramienta, empieza por la guia de introduccion a Claude Code. Para contrastar decisiones tecnicas usa las fuentes oficiales: web.dev Learn PWA, MDN sobre PWA instalables, MDN buenas practicas PWA, Chrome sobre criterios de instalacion y la documentacion oficial de Claude Code.

Arquitectura basica

Una PWA no es un archivo aislado, sino una pequena cadena de piezas. El HTML apunta al manifest, la aplicacion registra el Service Worker, el Service Worker controla peticiones dentro de su scope y decide si responde desde red, cache o una pagina fallback.

Usuario abre el sitio
  -> index.html enlaza manifest.webmanifest
  -> register-sw.js registra /sw.js
  -> sw.js precachea el app shell
  -> fetch aplica una estrategia por tipo de recurso
  -> navegacion offline recibe offline.html

Antes de pedir cambios a Claude Code, define tres decisiones.

DecisionEjemploRiesgo si se ignora
URL inicial y scope/ o /app/El Service Worker no controla las paginas esperadas
Recursos en cacheHTML, CSS, JS, imagenes, offline.htmlSe quedan 404 o versiones viejas
Comportamiento offlinePagina offline, ultima pagina, error APIEl usuario ve una pantalla rota

Para blogs, cursos y dashboards pequenos, una estrategia conservadora suele funcionar bien: Network First para navegacion HTML, Cache First para imagenes e iconos, y Stale While Revalidate para CSS, JavaScript y fuentes. Si quieres profundizar, combinalo con el articulo de estrategias de cache con Claude Code.

Prompt para Claude Code

Claude Code funciona mejor cuando le das archivos, reglas y criterios de verificacion. “Convierte esto en PWA” es demasiado abierto.

Convierte esta aplicacion Vite/React existente en una PWA.

Requisitos:
- Anadir public/manifest.webmanifest
- Referenciar iconos PNG 192x192, 512x512 y maskable 512x512
- Anadir public/offline.html
- Anadir public/sw.js como Service Worker
- Registrar el Service Worker desde src/register-sw.js
- Usar Network First para navegaciones HTML
- Usar Cache First para imagenes
- Usar Stale While Revalidate para CSS, JS y fuentes
- No cachear POST ni peticiones cross-origin
- Mostrar aviso cuando haya una nueva version del Service Worker
- Terminar con checklist manual para DevTools y Lighthouse

Restricciones:
- No usar un fetch handler vacio solo para parecer instalable
- Explicar cada archivo cambiado
- Indicar rutas que dependan del base path de produccion

Este prompt obliga a Claude Code a razonar sobre la estructura del proyecto. En una prueba de Masa con una landing de cursos, el problema inicial no fue la sintaxis del Service Worker, sino un desajuste entre start_url y scope. Pedir que esas suposiciones queden escritas evita cambios a ciegas.

Manifest e iconos

Crea public/manifest.webmanifest. name es el nombre completo de la app, short_name se usa en espacios pequenos, start_url es la pagina de arranque y scope define que URLs pertenecen a la aplicacion.

{
  "id": "/",
  "name": "ClaudeCodeLab PWA Demo",
  "short_name": "CCLab",
  "description": "Demo PWA offline creada con Claude Code",
  "start_url": "/?source=pwa",
  "scope": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#0f766e",
  "orientation": "portrait-primary",
  "prefer_related_applications": false,
  "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": "any maskable"
    }
  ]
}

Enlazalo desde el head del HTML.

<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0f766e" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />

No dejes rutas ficticias. Prepara PNG reales de 192x192 y 512x512, mas un icono maskable de 512x512 con margen seguro alrededor del logotipo. Despues de que Claude Code modifique el proyecto, abre cada URL de icono en el navegador y confirma que devuelve 200.

Service Worker con cache segura

Anade public/sw.js. Este ejemplo precachea el app shell, elimina caches antiguas y solo procesa peticiones GET del mismo origen.

const VERSION = "2026-06-02";
const STATIC_CACHE = `static-${VERSION}`;
const RUNTIME_CACHE = `runtime-${VERSION}`;

const APP_SHELL = [
  "/",
  "/offline.html",
  "/manifest.webmanifest",
  "/icons/icon-192.png",
  "/icons/icon-512.png",
  "/icons/icon-maskable-512.png"
];

self.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(STATIC_CACHE)
      .then((cache) => cache.addAll(APP_SHELL))
      .then(() => self.skipWaiting())
  );
});

self.addEventListener("activate", (event) => {
  const allowedCaches = [STATIC_CACHE, RUNTIME_CACHE];

  event.waitUntil(
    caches
      .keys()
      .then((keys) =>
        Promise.all(
          keys
            .filter((key) => !allowedCaches.includes(key))
            .map((key) => caches.delete(key))
        )
      )
      .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 !== self.location.origin) return;

  if (request.mode === "navigate") {
    event.respondWith(networkFirstPage(request));
    return;
  }

  if (request.destination === "image") {
    event.respondWith(cacheFirst(request));
    return;
  }

  if (["style", "script", "font"].includes(request.destination)) {
    event.respondWith(staleWhileRevalidate(request));
  }
});

async function networkFirstPage(request) {
  const cache = await caches.open(RUNTIME_CACHE);

  try {
    const response = await fetch(request);
    if (response.ok) await cache.put(request, response.clone());
    return response;
  } catch {
    const cached = await cache.match(request);
    return cached || (await caches.match("/offline.html")) || new Response("Offline", { status: 503 });
  }
}

async function cacheFirst(request) {
  const cached = await caches.match(request);
  if (cached) return cached;

  const response = await fetch(request);
  if (response.ok) {
    const cache = await caches.open(RUNTIME_CACHE);
    await cache.put(request, response.clone());
  }
  return response;
}

async function staleWhileRevalidate(request) {
  const cache = await caches.open(RUNTIME_CACHE);
  const cached = await cache.match(request);

  const networkPromise = fetch(request)
    .then((response) => {
      if (response.ok) cache.put(request, response.clone());
      return response;
    })
    .catch(() => undefined);

  if (cached) return cached;
  return (await networkPromise) || new Response("Network error", { status: 504 });
}

La parte importante es lo que no se cachea: formularios, pagos, sesiones, inventario, permisos y respuestas personalizadas. La cache del navegador es almacenamiento persistente. Si guardas datos equivocados, el bug puede seguir vivo incluso despues de desplegar una correccion.

Offline fallback y registro

public/offline.html debe ser simple y no depender de recursos externos.

<!doctype html>
<html lang="es">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Sin conexion</title>
  </head>
  <body>
    <main>
      <h1>Sin conexion</h1>
      <p>Vuelve a cargar cuando la red regrese. Algunas paginas recientes pueden seguir disponibles.</p>
      <p><a href="/">Volver al inicio</a></p>
    </main>
  </body>
</html>

Registra el worker desde src/register-sw.js.

export async function registerServiceWorker() {
  if (!("serviceWorker" in navigator)) return;

  window.addEventListener("load", async () => {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js", {
        scope: "/"
      });

      registration.addEventListener("updatefound", () => {
        const worker = registration.installing;
        if (!worker) return;

        worker.addEventListener("statechange", () => {
          if (worker.state === "installed" && navigator.serviceWorker.controller) {
            document.querySelector("[data-refresh-app]")?.removeAttribute("hidden");
          }
        });
      });
    } catch (error) {
      console.error("Service Worker registration failed:", error);
    }
  });
}

Y llamalo una sola vez desde el entrypoint.

import { registerServiceWorker } from "./register-sw.js";

registerServiceWorker();

El aviso de nueva version evita una clase de errores frecuente: una pestana antigua sigue controlada por el worker anterior mientras el despliegue ya publico archivos nuevos.

Instalacion, validacion y Lighthouse

Algunos navegadores basados en Chromium disparan beforeinstallprompt cuando la app cumple las condiciones. Usalo como mejora progresiva, no como unico camino de instalacion.

let deferredPrompt = null;

window.addEventListener("beforeinstallprompt", (event) => {
  event.preventDefault();
  deferredPrompt = event;
  document.querySelector("[data-install-app]")?.removeAttribute("hidden");
});

document.querySelector("[data-install-app]")?.addEventListener("click", async () => {
  if (!deferredPrompt) return;
  deferredPrompt.prompt();
  const choice = await deferredPrompt.userChoice;
  console.info("Install result:", choice.outcome);
  deferredPrompt = null;
});

Para validar, no dependas de una puntuacion PWA antigua. Revisa Manifest, Service Workers, Cache Storage y modo offline en DevTools. Usa Lighthouse para rendimiento, accesibilidad, buenas practicas y SEO.

npm run build
npx serve dist -l 4173
npx lighthouse http://localhost:4173 --view --only-categories=performance,accessibility,best-practices,seo
ComprobacionLugarCriterio
ManifestApplication > Manifestnombre, start_url e iconos sin errores
Service WorkerApplication > Service Workers/sw.js aparece activated
OfflineNetwork Offline y recargaraparece offline.html o una pagina reciente
Cache StorageApplication > Cache Storagecaches static y runtime esperadas
LighthouseInforme Lighthouseno empeoran rendimiento, SEO ni accesibilidad

Casos de uso, CTA y errores comunes

PWA tiene sentido cuando hay visitas repetidas, redes inestables o una razon clara para instalar. Tres casos fuertes son una biblioteca de cursos que se consulta en el transporte, un dashboard interno que se abre cada dia y una guia de evento donde la red del recinto puede fallar. Tambien funciona en comercio, siempre que no caches datos privados ni inventario critico.

Para monetizar, no vendas solo “instalabilidad”. Vende continuidad: mas lectura, mas retornos, menos abandono y CTAs medibles. En ClaudeCodeLab conviene medir clicks en instalar, uso offline, lectura completa y clicks hacia productos. Puedes revisar plantillas y packs en la biblioteca de productos.

Los errores comunes son repetitivos: scope y start_url no coinciden; HTML cacheado con Cache First; respuestas personalizadas guardadas en cache; iconos con 404; y depurar sin limpiar el Service Worker anterior. Cuando algo parezca inexplicable, haz Unregister, borra Cache Storage y repite el flujo de primera visita.

Resultado probado

En una prueba de Masa con una landing Vite para un curso, Claude Code genero rapido el manifest, la pagina offline y el registro. Lo lento fue verificar rutas de iconos, recargar en modo Offline y confirmar que un nuevo despliegue no quedaba atrapado por caches antiguas. La conclusion practica es empezar con un fallback offline pequeno y ampliar la cache solo cuando haya una mejora real para usuarios recurrentes.

#Claude Code #PWA #Service Worker #offline #mobile
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.