Las Progressive Web Apps (PWA) combinan lo mejor de la web y de las apps nativas: cargan al instante, funcionan sin conexión y pueden instalarse en el dispositivo del usuario. Convertir tu proyecto Django en PWA no es difícil, pero requiere cuidar varios detalles. Aquí tienes los tips esenciales, paso a paso.
- Django 4.x o superior.
- Navegador moderno con soporte de Service Workers.
- Servidor HTTPS en producción (los Service Workers requieren contexto seguro).
Si quieres velocidad, django-pwa o django-serviceworker agilizan el setup. Aquí usamos django-pwa como ejemplo.
pip install django-pwa
En settings.py añade la app y asegúrate de tener estáticos:
INSTALLED_APPS = [
# ...
"django.contrib.staticfiles",
"pwa", # <-- nuevo
]
Nota Django: en producción recuerda configurar STATIC_URL, STATIC_ROOT y ejecutar collectstatic.
El manifest.json describe tu app para que el navegador pueda “instalarla”. Guárdalo en tu carpeta static y enlázalo en la base.
Ejemplo de manifest.json:
{
"name": "Mi Django PWA",
"short_name": "DjangoPWA",
"start_url": "/",
"scope": "/",
"display": "standalone",
"theme_color": "#0e4c92",
"background_color": "#ffffff",
"icons": [
{ "src": "/static/img/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/static/img/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
En tu plantilla base:
<link rel="manifest" href="{% static 'manifest.json' %}">
<meta name="theme-color" content="#0e4c92">
Tip: añade también apple-touch-icon si te interesa iOS.
El Service Worker intercepta peticiones y gestiona caché/red. Crea static/sw.js (versión simple):
const CACHE_VERSION = "v1"; // cambia en cada build
const STATIC_CACHE = `static-${CACHE_VERSION}`;
const ASSETS = [
"/",
"/static/css/main.css",
"/static/js/main.js",
"/offline/"
];
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(STATIC_CACHE).then((cache) => cache.addAll(ASSETS))
);
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.map((k) => (k !== STATIC_CACHE ? caches.delete(k) : null)))
)
);
});
self.addEventListener("fetch", (event) => {
const req = event.request;
// Estrategias simples por tipo:
if (req.destination === "document") {
// Stale-While-Revalidate para HTML
event.respondWith(
caches.match(req).then((cached) => {
const fetchPromise = fetch(req).then((res) => {
const copy = res.clone();
caches.open(STATIC_CACHE).then((c) => c.put(req, copy));
return res;
}).catch(() => cached || caches.match("/offline/"));
return cached || fetchPromise;
})
);
return;
}
// Cache First para estáticos
if (["style", "script", "font", "image"].includes(req.destination)) {
event.respondWith(
caches.match(req).then((cached) => cached || fetch(req))
);
return;
}
// Network First por defecto (APIs)
event.respondWith(
fetch(req).catch(() => caches.match(req))
);
});
Registro en plantilla (respetando STATIC_URL):
<script>
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("{% static 'sw.js' %}", { scope: "/" });
});
}
</script>
SECURE_SSL_REDIRECT = Trueen producción.- CSP: permite solo orígenes necesarios (CSS/JS/IMG/CONNECT).
X-Frame-OptionsyReferrer-Policycoherentes.
Ejemplo básico (Django settings):
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
- Cache First → assets estáticos y fuentes.
- Stale-While-Revalidate → vistas HTML con cambios frecuentes.
- Network First → endpoints críticos (datos frescos).
Arriba ya tienes un ejemplo híbrido simple en el SW.
Sirve una página mínima cuando la red falla. Crea templates/offline.html y expónla como estático renderizado (o genera /offline/ al build).
// En el SW ya se intenta devolver "/offline/" en fallos de red
// Asegúrate de cachearla en ASSETS y que exista en producción
Chrome DevTools ► Lighthouse ► PWA. Apunta a > 90: SW activo, manifest válido, contraste correcto, tiempos de carga OK.
Cada build debe “romper” la caché vieja. Inyecta un hash (p.ej. GIT_SHA) en el nombre del cache o variable CACHE_VERSION.
const CACHE_VERSION = "v{{ GIT_SHA }}"; // sustituye en el pipeline
- HTTPS activo.
- Manifest válido con
start_urlyicons192/512. - Service Worker con al menos un
fetch/cachemanejado.
Si tu app lo necesita, integra django-webpush (VAPID). Flujo:
- Generar claves VAPID.
- Solicitar permiso en cliente y guardar la suscripción.
- Enviar notificaciones desde una
viewo tarea deCelery.
Convertir tu Django en PWA es añadir un manifest, registrar un Service Worker y pensar la caché con cabeza. En mi opinión, merece totalmente la pena: sensación nativa, resiliencia y velocidad. Empieza simple, itera, y tu proyecto estará listo para vivir en la pantalla de inicio de tus usuarios.
¡Nos leemos en el próximo post y, como siempre, rompe esos logs antes de que te rompan a ti!