Criar uma PWA com Claude Code: manifest, Service Worker e offline
Guia pratico para criar uma PWA com Claude Code: manifest, icones, Service Worker, fallback offline, cache e validacao.
Uma PWA, ou Progressive Web App, e uma aplicacao web que pode se aproximar da experiencia de um app instalado: nome e icone, abertura em janela standalone, recursos em cache e uma tela offline util quando a conexao falha.
O erro comum e achar que basta criar um manifest.webmanifest. Uma PWA confiavel tambem precisa de icones corretos, Service Worker, pagina de fallback offline, estrategia de cache, checagem de instalabilidade e validacao no Chrome DevTools e no Lighthouse. Um unico icone com 404 ou um HTML antigo preso no cache pode gerar erro so depois do deploy.
Este guia mostra como implementar uma PWA com Claude Code de um jeito pratico e verificavel. Se voce ainda esta comecando, leia antes o guia inicial de Claude Code. Para conferir as decisoes tecnicas, use as fontes oficiais: web.dev Learn PWA, MDN sobre PWA instalaveis, MDN boas praticas de PWA, Chrome sobre criterios de instalacao e a documentacao oficial do Claude Code.
Visao geral
Uma PWA e um conjunto de partes conectadas. O HTML aponta para o manifest, a aplicacao registra o Service Worker, e o Service Worker decide como responder a requisicoes dentro do seu scope: rede, cache ou fallback offline.
Usuario abre o site
-> index.html referencia manifest.webmanifest
-> register-sw.js registra /sw.js
-> sw.js precacheia o app shell
-> fetch escolhe estrategia por tipo de recurso
-> navegacao offline recebe offline.html
Antes de editar, defina tres pontos.
| Decisao | Exemplo | Risco se ignorar |
|---|---|---|
| URL inicial e scope | / ou /app/ | O Service Worker nao controla as paginas certas |
| Recursos em cache | HTML, CSS, JS, imagens, offline.html | Versoes antigas e 404 ficam persistentes |
| Comportamento offline | Pagina offline, ultima pagina, erro API | Usuario nao entende o estado do app |
Para blogs, cursos e dashboards pequenos, comece de forma conservadora: Network First para navegacao HTML, Cache First para imagens e icones, Stale While Revalidate para CSS, JavaScript e fontes. Para aprofundar, veja tambem o artigo de estrategias de cache com Claude Code.
Prompt para Claude Code
Claude Code responde melhor quando voce especifica arquivos, regras e verificacoes.
Transforme este app Vite/React existente em uma PWA.
Requisitos:
- Adicionar public/manifest.webmanifest
- Referenciar icones PNG 192x192, 512x512 e maskable 512x512
- Adicionar public/offline.html
- Adicionar public/sw.js como Service Worker
- Registrar o Service Worker em src/register-sw.js
- Usar Network First para navegacoes HTML
- Usar Cache First para imagens
- Usar Stale While Revalidate para CSS, JS e fontes
- Nao cachear POST nem requisicoes cross-origin
- Mostrar aviso quando uma nova versao do Service Worker estiver disponivel
- Finalizar com checklist manual para DevTools e Lighthouse
Restricoes:
- Nao adicionar fetch handler vazio so para parecer instalavel
- Explicar cada arquivo alterado
- Indicar caminhos que dependem do base path de producao
Em um teste de Masa com uma landing page pequena de curso, o primeiro problema nao foi sintaxe, mas a diferenca entre start_url e scope. Quando a prompt obriga Claude Code a expor essas premissas, a revisao fica bem mais objetiva.
Manifest e icones
Crie public/manifest.webmanifest. name e o nome completo, short_name aparece em areas estreitas, start_url e a pagina inicial do app instalado, e scope define o intervalo de URLs controlado.
{
"id": "/",
"name": "ClaudeCodeLab PWA Demo",
"short_name": "CCLab",
"description": "Demo PWA offline criada com 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"
}
]
}
Depois referencie no head.
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#0f766e" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
Use PNGs reais. No minimo, prepare 192x192 e 512x512, alem de um icone maskable 512x512 com area segura em volta da marca. Depois das alteracoes, abra cada URL de icone no navegador e confirme status 200.
Service Worker
Adicione public/sw.js. Este exemplo precacheia o app shell, remove caches antigos e so processa GET do mesmo origin.
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 });
}
Este codigo evita cachear POST, recursos externos e APIs privadas. Pagamento, login, carrinho, permissoes e estoque exigem regras explicitas de servidor e autenticacao. Cache nao e so velocidade; tambem e armazenamento persistente.
Offline fallback e registro
public/offline.html deve ser simples e independente de rede externa.
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Sem conexao</title>
</head>
<body>
<main>
<h1>Voce esta offline</h1>
<p>Recarregue quando a conexao voltar. Paginas abertas recentemente podem continuar disponiveis.</p>
<p><a href="/">Voltar ao inicio</a></p>
</main>
</body>
</html>
Registre em 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);
}
});
}
Chame uma vez no ponto de entrada.
import { registerServiceWorker } from "./register-sw.js";
registerServiceWorker();
O aviso de atualizacao evita misturar HTML antigo com JavaScript novo. Esse problema aparece muito em apps que ficam abertos por horas.
Instalacao e validacao
Alguns navegadores Chromium disparam beforeinstallprompt quando a app atende aos criterios. Use isso como melhoria progressiva.
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, nao dependa de uma pontuacao PWA antiga. Verifique Manifest, Service Worker, Cache Storage e modo Offline no DevTools. Use Lighthouse para performance, acessibilidade, boas praticas e SEO.
npm run build
npx serve dist -l 4173
npx lighthouse http://localhost:4173 --view --only-categories=performance,accessibility,best-practices,seo
| Checagem | Onde ver | Criterio |
|---|---|---|
| Manifest | Application > Manifest | nome, start_url e icones sem erro |
| Service Worker | Application > Service Workers | /sw.js em activated |
| Offline | Network Offline e reload | offline.html ou pagina recente aparece |
| Cache Storage | Application > Cache Storage | caches static/runtime esperados |
| Lighthouse | Relatorio | sem regressao de performance, SEO, acessibilidade |
Casos de uso, CTA e armadilhas
PWA vale mais em uso recorrente: biblioteca de cursos para ler no transporte, dashboard interno aberto todos os dias, guia de evento com rede instavel, ou comercio com muitas imagens. O CTA deve vender continuidade e mensuracao, nao apenas “instalar app”.
No ClaudeCodeLab, meca cliques de instalacao, acessos offline, leitura completa, retorno e cliques para produtos. Templates e packs de prompts ficam na biblioteca de produtos. Para times, entregue tambem revisao de cache, validacao do deploy e eventos de analytics.
Armadilhas comuns: scope e start_url divergentes; HTML em Cache First; respostas privadas no cache; icones com 404; e debug sem remover Service Worker antigo. Em caso de duvida, faca Unregister, limpe Cache Storage e repita o primeiro acesso.
Resultado testado
Em uma landing Vite pequena de curso, Masa viu Claude Code gerar rapidamente manifest, pagina offline e registro. O trabalho real foi conferir URLs de icones, recarregar em Offline e limpar caches antigos depois de novas versoes. A pratica mais segura e lancar primeiro um fallback offline minimo e ampliar o cache apenas para recursos que melhoram de fato a experiencia de quem volta.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.