biotech Django & Python

Converting Django Projects to PWA: 2026 Complete Guide

AK
Ali Kasımoğlu
13 Feb 2021 schedule 9 min read
Converting Django to PWA 2026 Complete Guide - AnomixLabs
analytics

Insight Density

groups Target Audience: Intermediate
65 Score

Calculated by technical complexity and content density.

Last updated: April 2026 · AnomixLabs Technical Team

PWA is the most pragmatic way to deliver an app-like experience without sacrificing the universal accessibility of the web, especially for those with a Django backend.

What is a PWA?

Progressive Web Apps (PWAs) are web applications developed with modern web technologies that offer a native app-like experience. They consist of three core components: Web App Manifest (installation information), Service Worker (background processing and offline caching), and HTTPS (security requirement).

PWA advantages: App Store independence, no 30% commission, instant updates, single codebase for all platforms, indexable by search engines.

iOS 17.4 PWA Changes: An Important Update

Apple announced the removal of home screen PWA support in Europe with iOS 17.4 (February 2024), then reversed this decision under pressure from the DMA (Digital Markets Act) (March 2024). The current situation is:

  • PWA home screen installation is still supported in all regions on iOS.
  • Service worker support has been improved in iOS 17.4+.
  • Push notifications: Web Push API support has been available since iOS 16.4+, but requires user permission and has some limitations.
  • Full-screen mode (standalone) works on iOS, but some native APIs are still missing (Face ID, NFC, etc.).

Django URL Configuration

The three PWA files (manifest.json, sw.js, offline/) must be served through Django's URL router, not as static files, because the browser expects these files from the root scope:

from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    # manifest.json — the browser looks for this URL
    path('manifest.json', TemplateView.as_view(
        template_name='manifest.json',
        content_type='application/manifest+json'
    ), name='manifest'),

    # Service Worker — must be in the root scope (/sw.js)
    path('sw.js', TemplateView.as_view(
        template_name='sw.js',
        content_type='application/javascript'
    ), name='service-worker'),

    # Offline page — kept in cache
    path('offline/', TemplateView.as_view(
        template_name='offline.html'
    ), name='offline'),
]

Define these URLs outside of i18n_patterns(), without language prefixes. Otherwise, the browser will look for /tr/sw.js instead of /sw.js, and the service worker registration will fail.

Web App Manifest

The manifest file tells the browser how to install your application:

{
  "name": "AnomixLabs App",
  "short_name": "Anomix",
  "description": "Web application powered by Django",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#1a1a2e",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/static/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/static/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

Manifest Field Reference

Explanation of the most commonly used manifest.json fields:

PWA standalone display mode — browser UI hidden PWA minimal-ui display mode — minimal browser UI PWA theme_color — browser toolbar and Android notification bar color PWA app shortcuts — shortcut menu on long press

Field Description
name The full name of the application. Appears on the splash screen and loading screen. If not specified, short_name is used.
short_name The short name displayed under the icon on the home screen. Recommended to be no more than 12 characters.
start_url The URL that opens when the app icon is tapped. "/" points to the homepage.
display standalone: address bar hidden — most common usage. fullscreen: full screen. minimal-ui: small back button. browser: normal browser view.
background_color The background color of the splash screen. Visible before CSS loads.
theme_color The color of the browser toolbar in Android Chrome and the taskbar color. Should match the <meta name="theme-color"> in HTML.
icons A list of icons for different sizes. Chrome requires a minimum of 192x192 and 512x512. purpose: "maskable" is required for round icons on Android. All sizes: 72, 96, 128, 144, 152, 192, 384, 512 pixels.
shortcuts Quick access menu that appears when the icon is long-pressed. For each shortcut, name, url, and icons are defined.
orientation Supported screen orientation. "any" works in both portrait and landscape.
prefer_related_applications If set to true, the browser will suggest the Play Store/App Store application instead of the PWA. Usually left as false.
lang / dir The language of the content ("tr-TR", "en-US") and text direction ("ltr" or "rtl").

Serve this file with staticfiles in Django and add a link to the HTML head:

<!-- Add to the <head> section of base.html -->
<link rel="manifest" href="{% url 'manifest' %}">
<meta name="theme-color" content="#1a1a2e">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="AnomixLabs">
<link rel="apple-touch-icon" href="{% static 'icons/icon-192.png' %}">

Service Worker: Basic Structure

A Service Worker is a JavaScript file that runs in the background, independent of the main thread, and intercepts network requests:

// sw.js — Service Worker
const CACHE_NAME = 'anomix-v1';
const STATIC_ASSETS = [
  '/',
  '/static/css/main.css',
  '/static/js/app.js',
  '/offline/',
];

// Install: Cache static assets
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
  );
  self.skipWaiting();
});

// Activate: Clean up old caches
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then(keys =>
      Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
    )
  );
  self.clients.claim();
});

// Fetch: Cache first, then network (Cache-First strategy)
self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return;
  event.respondWith(
    caches.match(event.request).then(cached => {
      if (cached) return cached;
      return fetch(event.request).catch(() => caches.match('/offline/'));
    })
  );
});

Registering the Service Worker in Django:

// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js', { scope: '/' })
      .then(reg => console.log('SW registered:', reg.scope))
      .catch(err => console.log('SW failed:', err));
  });
}

Important: The Service Worker file needs to be served at the root path (/sw.js) before Django URLs, requiring URL configuration:

# urls.py
from django.views.generic import TemplateView

urlpatterns = [
    path('sw.js', TemplateView.as_view(
        template_name='sw.js',
        content_type='application/javascript'
    ), name='service-worker'),
]

Workbox: A Library to Simplify Service Worker Management

Google's Workbox library provides service worker strategies with ready-to-use APIs:

// sw.js with Workbox
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';

// Precache static assets
precacheAndRoute(self.__WB_MANIFEST);

// Images: Cache-First (30 days)
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 3600 })]
  })
);

// API requests: Network-First
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({ cacheName: 'api-cache', networkTimeoutSeconds: 3 })
);

// Pages: Stale-While-Revalidate
registerRoute(
  ({ request }) => request.mode === 'navigate',
  new StaleWhileRevalidate({ cacheName: 'pages' })
);

Cache Strategies

  • Cache-First: Check cache first, then fetch from the network. Ideal for logos, fonts, static CSS/JS.
  • Network-First: Fetch from the network first, fall back to cache if it fails. For APIs and dynamic pages.
  • Stale-While-Revalidate: Respond from cache immediately, then update in the background. Ideal for news pages.
  • Cache-Only: Only use cache — for pre-loaded offline content.
  • Network-Only: Do not use cache — for critical operations like payment pages.

Web Push API: Push Notifications with Django

The Web Push API allows users to receive notifications even when the browser is closed. It requires a VAPID (Voluntary Application Server Identification) key:

# pip install pywebpush
from pywebpush import webpush, WebPushException

def send_push_notification(subscription_info, message):
    try:
        webpush(
            subscription_info=subscription_info,
            data=json.dumps({'title': 'Anomix', 'body': message}),
            vapid_private_key=settings.VAPID_PRIVATE_KEY,
            vapid_claims={'sub': 'mailto:info@anomixlabs.com'}
        )
    except WebPushException as ex:
        logger.error('Push notification failed: %s', ex)

iOS limitation: On iOS, push notifications only work for PWAs added to the home screen; they do not work on an open page in the browser (iOS 16.4+). On Android, they work from the browser as well.

App Badging API

The App Badging API adds a count of unread items to the app icon — for email or messaging apps:

// Write the notification count to the badge
if ('setAppBadge' in navigator) {
  navigator.setAppBadge(unreadCount);
}

// Clear the badge
if ('clearAppBadge' in navigator) {
  navigator.clearAppBadge();
}

Offline Experience: The /offline/ Page

A fallback page to display when the user is offline and the requested page is not in the cache:

PWA offline page — displayed when there is no internet connection

{% extends 'base.html' %}
{% block content %}
<div class="flex flex-col items-center justify-center min-h-[60vh] text-center px-4">
  <svg xmlns="http://www.w3.org/2000/svg" class="w-16 h-16 text-on-surface/20 mb-6"
       fill="none" viewBox="0 0 24 24" stroke="currentColor">
    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
          d="M3 15a4 4 0 004 4h10a5 5 0 002.45-9.34A7 7 0 106.33 15H3z" />
  </svg>
  <h1 class="text-2xl font-bold mb-2">No internet connection</h1>
  <p class="text-on-surface/60 mb-8">This page is not available offline.</p>
  <button onclick="window.location.reload()"
          class="px-6 py-3 bg-primary text-white rounded-xl font-medium
                 hover:opacity-90 transition-opacity">
    Try again
  </button>
</div>
{% endblock %}

Install Prompt: beforeinstallprompt

To prompt users to add the PWA to their home screen, capture the beforeinstallprompt event. The browser triggers this event automatically when PWA criteria are met — your job is to prevent the default behavior and show your own UI:

PWA install prompt UI — beforeinstallprompt PWA app install button — Download App design

<!-- Add before </body> in base.html -->
<div id="pwa-banner" class="hidden fixed bottom-5 left-5 z-50
     flex items-center gap-3 px-4 py-3 rounded-2xl
     bg-surface-container border border-on-surface/10 shadow-lg">
  <span class="text-sm text-on-surface">Add the app to your home screen</span>
  <button id="pwa-install"
          class="px-4 py-1.5 bg-primary text-white text-sm rounded-xl font-medium">
    Install
  </button>
  <button id="pwa-dismiss" class="text-on-surface/40 text-lg leading-none">
    &times;
  </button>
</div>
let deferredPrompt;
const banner     = document.getElementById('pwa-banner');
const installBtn = document.getElementById('pwa-install');
const dismissBtn = document.getElementById('pwa-dismiss');

// When the browser has prepared the install prompt
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();       // Prevent the default banner
  deferredPrompt = e;
  banner.classList.remove('hidden');
});

// When the "Install" button is clicked
installBtn?.addEventListener('click', async () => {
  banner.classList.add('hidden');
  deferredPrompt.prompt();
  const { outcome } = await deferredPrompt.userChoice;
  console.log('PWA install:', outcome);  // 'accepted' | 'dismissed'
  deferredPrompt = null;
});

// Listen for the dismiss button
dismissBtn?.addEventListener('click', () => banner.classList.add('hidden'));

// When the app is successfully installed
window.addEventListener('appinstalled', () => {
  banner.classList.add('hidden');
  console.log('PWA installed');
});

// Detect if opened as PWA or in a normal browser
const isPwa = navigator.standalone
  || window.matchMedia('(display-mode: standalone)').matches;
console.log('Launch mode:', isPwa ? 'PWA' : 'browser');

Browser support: beforeinstallprompt works in Chrome/Edge (Android and desktop). Safari does not trigger this event — manually show iOS users the "Share → Add to Home Screen" prompt (e.g., with a check for navigator.standalone === false).

Lighthouse PWA Audit for a Perfect Score

Lighthouse PWA audit criteria:

  • HTTPS required
  • Service Worker registered and active
  • Web App Manifest filled with all required fields
  • 192x192 and 512x512 maskable icons
  • start_url available in cache if no redirect
  • HTTP → HTTPS redirect
  • theme-color meta tag

Summary

Converting a Django project to a PWA involves 3 main steps: write manifest.json, add a service worker (/sw.js), and verify HTTPS. Workbox simplifies service worker management in production. Push notifications require the pywebpush library and VAPID keys. PWA support is stable after iOS 17.4, but some native API limitations on iOS persist.

Frequently Questions

Does iOS support PWA push notifications? expand_more
Since iOS 16.4, yes — but with conditions: the user must first add the PWA to their home screen. Push notifications don't work on open Safari tabs (browser context) — only from the installed home screen PWA. This is a fundamental difference from Android, where push works in both contexts. Test on a real iPhone before releasing to users.
Can a PWA be listed on the App Store? expand_more
Not directly, but workarounds exist. Google Play Store: package your PWA as a TWA (Trusted Web Activity) — Chrome-based and installable via Play. Apple App Store: wrap with WKWebView as a hybrid app — but this removes some PWA features and requires App Store review. For most cases, direct installation from the browser (Add to Home Screen) is the preferred PWA distribution method.
How should I define a Service Worker update strategy? expand_more
When a new service worker loads, old pages still use the old SW — an update requires browser restart or closing all tabs. The skipWaiting() + clients.claim() combination activates the new SW immediately — but can break pages that expected the old cache. Best practice: show a 'New version available — refresh' banner to users rather than forcing an invisible reload.
Should I choose offline-first or cache-first? expand_more
It depends on the application type. Offline-first: app always runs from cache, syncs in the background — ideal for note-taking apps, news readers. Cache-first: serve from cache, fall back to network if not cached — good for static-heavy sites. Network-first: always try network, fall back to cache — best for frequently updated content where stale data is problematic. Most sites benefit from a hybrid: cache-first for static assets, network-first for API calls.
What are the disadvantages of PWA? expand_more
No App Store discoverability (major distribution gap for consumer apps), limited access to some native APIs (Bluetooth, NFC, Face ID) — though Web APIs are expanding rapidly, limited background sync and geofencing on iOS, some enterprise MDM systems restrict PWA installation, and push notification opt-in rates tend to be lower than native app equivalents.
Should I use the django-pwa package? expand_more
django-pwa provides a minimal starting point for manifest and service worker setup. However, as of 2024–2026, manual setup with Workbox is more flexible and gives better control over caching strategies. django-pwa hasn't had significant updates recently. For new projects, the recommended approach is: write manifest.json manually, generate service-worker.js with Workbox CLI, and serve both as static files.
How do I solve the CSRF token problem with Service Worker? expand_more
When a Service Worker intercepts fetch events, POST requests requiring CSRF tokens can lose the token. Solution: don't intercept POST requests in your SW — apply cache strategies only to GET requests. For forms that must work offline, use Background Sync API to queue submissions and replay them when connectivity is restored, retrieving a fresh CSRF token at replay time.
Tags: #Django #PWA #Progressive Web App #Service Worker #Web Manifest #Push Notification #Workbox #iOS #Offline
share

Share This Article

Support us by sharing this with your network.

AK

Ali Kasımoğlu

Full-stack Developer & Founder of AnomixLabs

A software developer specializing in the Python and Django ecosystem. Focuses on modern web architectures, AI integrations, and minimalist user experiences. Under the AnomixLabs umbrella, he aims to transform complex problems into lean and effective digital solutions.

psychology
psychology

Ask a Question About the Article

AnomixAI · Answers based on the article content

5 questions left
Only about article content 0/500
forward_to_inbox

The Future Decoded.

Join 5,000+ engineers and founders receiving the monthly briefing on enterprise AI, software architecture and digital transformation. No spam.