Progressive Web Apps kombinieren das Beste aus Web und nativen Apps. In diesem Guide zeigen wir, wie Sie PWAs mit Next.js entwickeln, die offline funktionieren, installierbar sind und Push Notifications senden.
Was ist eine PWA?
Eine PWA ist eine Web-App mit drei Kernmerkmalen:
- Installierbar - Auf Home Screen wie native App
- Offline-fähig - Funktioniert ohne Internet
- Engagement - Push Notifications möglich
PWA vs Native App
| Feature | PWA | Native App | |---------|-----|------------| | Installation | Browser → Home Screen | App Store | | Updates | Automatisch | User muss updaten | | Größe | 1-5 MB | 50-200 MB | | Offline | ✅ Möglich | ✅ Standard | | Hardware | ⚠️ Limitiert | ✅ Voller Zugriff | | Distribution | ✅ URL teilen | ❌ Store-Approval |
PWA Requirements
1. HTTPS
# Local Development
npx next dev
# Production (Docker Build & Deploy)
docker build -t my-pwa .
docker run -p 3000:3000 my-pwa
2. Web App Manifest
// public/manifest.json
{
"name": "HEADON.pro Marketing Agency",
"short_name": "HEADON",
"description": "Moderne Webentwicklung & Mobile Apps",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3b82f6",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
// app/layout.tsx
export const metadata = {
manifest: '/manifest.json',
}
3. Service Worker
pnpm add next-pwa
// next.config.js
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
})
module.exports = withPWA({
// Your Next.js config
})
Service Worker Strategies
Cache-First (Static Assets)
// public/sw.js
import { CacheFirst } from 'workbox-strategies'
import { registerRoute } from 'workbox-routing'
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
{
cacheWillUpdate: async ({ response }) => {
return response.status === 200 ? response : null
},
},
],
})
)
Network-First (API Calls)
import { NetworkFirst } from 'workbox-strategies'
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api',
networkTimeoutSeconds: 3,
})
)
Stale-While-Revalidate
import { StaleWhileRevalidate } from 'workbox-strategies'
registerRoute(
({ request }) => request.destination === 'script' || request.destination === 'style',
new StaleWhileRevalidate({
cacheName: 'static-resources',
})
)
Offline Support
Offline Fallback Page
// app/offline/page.tsx
export default function OfflinePage() {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<WifiOff className="w-16 h-16 mx-auto text-gray-400" />
<h1 className="mt-4 text-2xl font-bold">Keine Internetverbindung</h1>
<p className="mt-2 text-gray-600">
Bitte überprüfen Sie Ihre Verbindung und versuchen Sie es erneut.
</p>
<Button
className="mt-4"
onClick={() => window.location.reload()}
>
Erneut versuchen
</Button>
</div>
</div>
)
}
Background Sync
// Service Worker
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages())
}
})
async function syncMessages() {
const db = await openDB('messages')
const messages = await db.getAll('pending')
for (const message of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message),
})
await db.delete('pending', message.id)
} catch (error) {
// Will retry on next sync
}
}
}
// Client
async function sendMessage(message) {
try {
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify(message),
})
} catch (error) {
// Save to IndexedDB
await db.add('pending', message)
// Register sync
await navigator.serviceWorker.ready
await registration.sync.register('sync-messages')
}
}
Push Notifications
Server Setup
// app/api/subscribe/route.ts
import webpush from 'web-push'
webpush.setVapidDetails(
'mailto:hallo@headon.pro',
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
)
export async function POST(req: Request) {
const subscription = await req.json()
// Save subscription to database
await db.subscription.create({ data: subscription })
return Response.json({ success: true })
}
Client Setup
'use client'
export function PushNotificationSubscribe() {
const subscribe = async () => {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.NEXT_PUBLIC_VAPID_KEY,
})
await fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json' },
})
}
return (
<Button onClick={subscribe}>
Benachrichtigungen aktivieren
</Button>
)
}
Send Push Notification
// Server-side
import webpush from 'web-push'
async function sendNotification(userId: string, payload: any) {
const subscriptions = await db.subscription.findMany({
where: { userId },
})
await Promise.all(
subscriptions.map((sub) =>
webpush.sendNotification(
sub,
JSON.stringify({
title: 'Neue Nachricht',
body: payload.message,
icon: '/icon-192.png',
data: { url: '/messages' },
})
)
)
)
}
Handle Push in Service Worker
// Service Worker
self.addEventListener('push', (event) => {
const data = event.data.json()
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: data.icon,
data: data.data,
})
)
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
event.waitUntil(
clients.openWindow(event.notification.data.url)
)
})
Installation Prompt
'use client'
export function InstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
const [showInstall, setShowInstall] = useState(false)
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault()
setDeferredPrompt(e)
setShowInstall(true)
}
window.addEventListener('beforeinstallprompt', handler)
return () => window.removeEventListener('beforeinstallprompt', handler)
}, [])
const handleInstall = async () => {
if (!deferredPrompt) return
deferredPrompt.prompt()
const { outcome } = await deferredPrompt.userChoice
if (outcome === 'accepted') {
console.log('App installed')
}
setDeferredPrompt(null)
setShowInstall(false)
}
if (!showInstall) return null
return (
<div className="fixed bottom-0 left-0 right-0 p-4 bg-primary-600 text-white">
<div className="container mx-auto flex items-center justify-between">
<div>
<p className="font-semibold">App installieren</p>
<p className="text-sm">Für schnelleren Zugriff auf dem Home Screen</p>
</div>
<Button
variant="outline"
className="bg-white text-primary-600"
onClick={handleInstall}
>
Installieren
</Button>
</div>
</div>
)
}
PWA Best Practices
1. App-Like Experience
/* globals.css - Disable pull-to-refresh */
body {
overscroll-behavior-y: contain;
}
/* Safe area for notched devices */
.header {
padding-top: env(safe-area-inset-top);
}
2. Loading Performance
// Preload critical resources
const addResourcesToCache = async (resources) => {
const cache = await caches.open('v1')
await cache.addAll(resources)
}
self.addEventListener('install', (event) => {
event.waitUntil(
addResourcesToCache([
'/',
'/offline',
'/styles/main.css',
'/scripts/main.js',
])
)
})
3. Update Handling
'use client'
export function UpdatePrompt() {
const [showUpdate, setShowUpdate] = useState(false)
useEffect(() => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').then((registration) => {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
setShowUpdate(true)
}
})
})
})
}
}, [])
const handleUpdate = () => {
window.location.reload()
}
if (!showUpdate) return null
return (
<div className="fixed top-4 right-4 bg-white shadow-lg rounded-lg p-4">
<p className="font-semibold">Update verfügbar</p>
<Button onClick={handleUpdate} className="mt-2">
Jetzt aktualisieren
</Button>
</div>
)
}
Testing PWA
# Lighthouse PWA Audit
npx lighthouse https://your-app.com --view
# Chrome DevTools
# Application Tab → Service Workers, Manifest
# Mobile Testing
# Chrome → Remote Devices
PWA Checklist:
- [ ] HTTPS aktiviert
- [ ] Manifest.json vorhanden
- [ ] Service Worker registriert
- [ ] Offline-Fallback
- [ ] Icons (192px, 512px)
- [ ] Theme Color gesetzt
- [ ] Meta Tags (viewport, theme-color)
- [ ] Lighthouse Score 90+
Zusammenfassung
PWAs bieten:
✅ App-Like Experience ohne App Store ✅ Offline-Support für bessere UX ✅ Push Notifications für Engagement ✅ Schnelle Updates ohne User-Action
Perfekt für E-Commerce, News, Social Apps.
PWA Entwicklung?
Als Mobile-Experten entwickeln wir:
- 📱 Progressive Web Apps
- 🔔 Push Notification Systems
- 📴 Offline-First Architekturen
- ⚡ Performance-Optimierung
Aktualisiert: Dezember 2023