Introducción

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.

Requisitos Previos
  • Django 4.x o superior.
  • Navegador moderno con soporte de Service Workers.
  • Servidor HTTPS en producción (los Service Workers requieren contexto seguro).
1. Instalar las dependencias básicas

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.

2. Crear y exponer el manifest

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.

3. Registrar el Service Worker

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>
4. Ajustar cabeceras y seguridad
  • SECURE_SSL_REDIRECT = True en producción.
  • CSP: permite solo orígenes necesarios (CSS/JS/IMG/CONNECT).
  • X-Frame-Options y Referrer-Policy coherentes.

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
5. Estrategias de caché inteligentes
  • 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.

6. Modo offline “agradable”

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
7. Testear con Lighthouse

Chrome DevTools ► Lighthouse ► PWA. Apunta a > 90: SW activo, manifest válido, contraste correcto, tiempos de carga OK.

8. CI/CD y versioning de caché

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
9. Conseguir el “Add to Home Screen”
  • HTTPS activo.
  • Manifest válido con start_url y icons 192/512.
  • Service Worker con al menos un fetch/cache manejado.
10. Bonus: Push Notifications

Si tu app lo necesita, integra django-webpush (VAPID). Flujo:

  1. Generar claves VAPID.
  2. Solicitar permiso en cliente y guardar la suscripción.
  3. Enviar notificaciones desde una view o tarea de Celery.

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!