Capítulo 72 de 456
Build a Progressive Web App with Next.js: a web app manifest for installability, Web Push notifications via a service worker + Server Actions + VAPID keys, and security headers, without needing a separate native codebase.
app/manifest.ts: built-in file convention that generates the web app manifest (name, icons, display: 'standalone', colors) enabling home-screen installation.lib/service-worker.js, registered via navigator.serviceWorker.register, listens for push and notificationclick events to show/handle notifications.web-push generate-vapid-keys CLI; public key goes in NEXT_PUBLIC_VAPID_PUBLIC_KEY, private key in VAPID_PRIVATE_KEY.subscribeUser, unsubscribeUser, sendNotification in app/actions.ts ('use server'), using the web-push package server-side.useOffline (experimental): hook + matching experimental.useOffline config for connectivity-aware UI and automatic retry of failed navigations/Server Actions; not full offline caching.import type { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Next.js PWA',
short_name: 'NextPWA',
description: 'A Progressive Web App built with Next.js',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#000000',
icons: [
{ src: '/icon-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512x512.png', sizes: '512x512', type: 'image/png' },
],
}
}
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!),
})
setSubscription(sub)
await subscribeUser(JSON.parse(JSON.stringify(sub)))
}
'use server'
import webpush from 'web-push'
webpush.setVapidDetails(
'<mailto:your-email@example.com>',
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
)
export async function sendNotification(message: string) {
if (!subscription) throw new Error('No subscription available')
await webpush.sendNotification(subscription, JSON.stringify({ title: 'Test Notification', body: message, icon: '/icon.png' }))
return { success: true }
}
web-push; em produção a subscription deve ir para um banco, não variável em memória.self.addEventListener('push', function (event) {
if (event.data) {
const data = event.data.json()
event.waitUntil(self.registration.showNotification(data.title, { body: data.body, icon: data.icon || '/icon.png' }))
}
})
self.addEventListener('notificationclick', function (event) {
event.notification.close()
event.waitUntil(clients.openWindow('<https://your-website.com>'))
})
module.exports = {
async headers() {
return [
{ source: '/(.*)', headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
]},
{ source: '/sw.js', headers: [
{ key: 'Content-Type', value: 'application/javascript; charset=utf-8' },
{ key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' },
{ key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self'" },
]},
]
},
}
sw.js).| Requisito de instalação | Detalhe |
|---|---|
| Web app manifest válido | Criado via app/manifest.ts |
| Servido via HTTPS | Obrigatório; teste local com next dev --experimental-https |
beforeinstallprompt para botão customizado de instalação: não é cross-browser/plataforma (não funciona no Safari iOS); a doc recomenda não usar.sw.js: força usuários a ficar com versão antiga do service worker; sempre no-cache, no-store, must-revalidate.NEXT_PUBLIC_VAPID_PUBLIC_KEY) quanto no servidor (VAPID_PRIVATE_KEY).useOffline (experimental) cobre UI ciente de conectividade e retry automático; para cache offline completo via service worker, usar Serwist.