NextJS App Router PWA Guide
For my non-profit project I've been building a pwa with nextjs app router. The journey so far has been full of frustration and banging my head against the wall. I hope this collection of learnings will help you avoid some of the pitfalls I encountered.
I'll try to update this page, whenever I stumble upon something new. So make sure to come back periodically.
Last updated: 2024-05-11
Getting apple-touch-startup-image
to work on iOS
This one was a real pain. Not only do you have to supply the exact dimensions of images for EVERY SINGLE iOS device, but you also have to make sure the metadata
config is correct. Apple seems to ignore the metadata.json
file for the most part. Because #apple.
Here's what worked for me:
I've generated the images automatically with https://progressier.com/pwa-icons-and-ios-splash-screen-generator
Extract the resulting files to public/pwa/ios/splash_screens/
. The tool generates a txt
file with the meta tags, but nextjs wants the metatags as metadata
config in your root layout.
I've done this with some vscode multicursor magic.
export const metadata : Metadata = {
title: "THL Praktika" ,
description:
"Vereinfachte Pratika Planung für Trainer:innen und Stundent:innen." ,
metadataBase: new URL (process.env. NEXTAUTH_URL || "http://localhost:3000" ),
applicationName: "THL Praktika" ,
manifest: "/manifest.json" ,
appleWebApp: {
capable: true ,
statusBarStyle: "default" ,
title: "THL Praktika" ,
startupImage: [
"/pwa/ios/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png" ,
{
media:
"screen and (device-width: 430px) and (device-height: 932px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" ,
url: "/pwa/ios/splash_screens/iPhone_15_Pro_Max__iPhone_15_Plus__iPhone_14_Pro_Max_portrait.png" ,
},
{
media:
"screen and (device-width: 393px) and (device-height: 852px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" ,
url: "/pwa/ios/splash_screens/iPhone_15_Pro__iPhone_15__iPhone_14_Pro_portrait.png" ,
},
{
media:
"screen and (device-width: 428px) and (device-height: 926px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" ,
url: "/pwa/ios/splash_screens/iPhone_14_Plus__iPhone_13_Pro_Max__iPhone_12_Pro_Max_portrait.png" ,
},
{
media:
"screen and (device-width: 390px) and (device-height: 844px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" ,
url: "/pwa/ios/splash_screens/iPhone_14__iPhone_13_Pro__iPhone_13__iPhone_12_Pro__iPhone_12_portrait.png" ,
},
{
media:
"screen and (device-width: 375px) and (device-height: 812px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" ,
url: "/pwa/ios/splash_screens/iPhone_13_mini__iPhone_12_mini__iPhone_11_Pro__iPhone_XS__iPhone_X_portrait.png" ,
},
{
media:
"screen and (device-width: 414px) and (device-height: 896px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" ,
url: "/pwa/ios/splash_screens/iPhone_11_Pro_Max__iPhone_XS_Max_portrait.png" ,
},
{
media:
"screen and (device-width: 414px) and (device-height: 896px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/iPhone_11__iPhone_XR_portrait.png" ,
},
{
media:
"screen and (device-width: 414px) and (device-height: 736px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 3)" ,
url: "/pwa/ios/splash_screens/iPhone_8_Plus__iPhone_7_Plus__iPhone_6s_Plus__iPhone_6_Plus_portrait.png" ,
},
{
media:
"screen and (device-width: 375px) and (device-height: 667px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/iPhone_8__iPhone_7__iPhone_6s__iPhone_6__4.7__iPhone_SE_portrait.png" ,
},
{
media:
"screen and (device-width: 1032px) and (device-height: 1376px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/13__iPad_Pro_M4_portrait.png" ,
},
{
media:
"screen and (device-width: 1024px) and (device-height: 1366px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/12.9__iPad_Pro_portrait.png" ,
},
{
media:
"screen and (device-width: 834px) and (device-height: 1210px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/11__iPad_Pro_M4_portrait.png" ,
},
{
media:
"screen and (device-width: 834px) and (device-height: 1194px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/11__iPad_Pro__10.5__iPad_Pro_portrait.png" ,
},
{
media:
"screen and (device-width: 820px) and (device-height: 1180px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/10.9__iPad_Air_portrait.png" ,
},
{
media:
"screen and (device-width: 834px) and (device-height: 1112px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/10.5__iPad_Air_portrait.png" ,
},
{
media:
"screen and (device-width: 810px) and (device-height: 1080px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/10.2__iPad_portrait.png" ,
},
{
media:
"screen and (device-width: 768px) and (device-height: 1024px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/9.7__iPad_Pro__7.9__iPad_mini__9.7__iPad_Air__9.7__iPad_portrait.png" ,
},
{
media:
"screen and (device-width: 744px) and (device-height: 1133px) and (orientation: portrait) and (-webkit-device-pixel-ratio: 2)" ,
url: "/pwa/ios/splash_screens/8.3__iPad_Mini_portrait.png" ,
},
],
},
other: {
"mobile-web-app-capable" : "yes" ,
},
};
Important
The thing that almost broke me was the fact that none of this will work unless you add the "mobile-web-app-capable": "yes"
to the other
field.
Service Worker with serwist
Last time i checked, next-pwa
did not support app router. So I had to use @serwist/next
and serwist
instead.
Following the getting started guide on https://serwist.pages.dev/docs/next/getting-started worked for me out of the box.
Here's my app/sw.ts
file for reference.
import { defaultCache } from "@serwist/next/worker" ;
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist" ;
import { Serwist } from "serwist" ;
declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST : ( PrecacheEntry | string )[] | undefined ;
}
}
declare const self : ServiceWorkerGlobalScope ;
const serwist = new Serwist ({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true ,
clientsClaim: true ,
navigationPreload: true ,
runtimeCaching: defaultCache,
});
serwist. addEventListeners ();
And here's an excerpt of my next.config.mjs
for reference.
import withSerwistInit from "@serwist/next" ;
// ... other stuff like sentry, mdx, ...
const withPWA = withSerwistInit ({
swSrc: "src/app/sw.ts" ,
swDest: "public/sw.js" ,
disable: process.env. NODE_ENV === "development" ,
});
export default withPWA ( nextConfig ) ;