# Next.js PWA ohne next-pwa: Offline-Seite und App Shell

> Eine Next.js PWA auf Next.js 16 ohne next-pwa: app/manifest.ts, ein eigener Offline-Service-Worker, ein Install Prompt und eine App Shell mit Safe Areas.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 5. Nov. 2026
- Kategorie: [Mobile UI mit React](https://wingo-ui.com/de/blog/category/mobile-ui)
- Lesezeit: 12 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/nextjs-pwa
- Englische Fassung: https://wingo-ui.com/blog/nextjs-pwa

## Kurz gesagt

Eine Next.js PWA mit dem App Router braucht vier eigene Dateien: app/manifest.ts mit Name, Icons und standalone-Display, eine eigenständige public/offline.html, eine kleine public/sw.js, die diese Seite ausliefert, wenn ein Seitenaufruf scheitert, und eine Client-Komponente, die den Worker registriert. Lass next-pwa auf Next.js 16 weg: Es ist ein Webpack-Plugin, und next build läuft inzwischen mit Turbopack, also schreibst du den Worker selbst und steigst erst auf Serwist um, wenn du gecachte Routen und Assets brauchst. Setz im viewport-Export viewportFit auf cover und ergänze dann einen Install Prompt und eine App Shell, deren Top Bar und Bottom Tabs die Safe Areas freihalten.

Eine Next.js PWA ist eine ganz normale App-Router-App mit ein paar zusätzlichen Dateien: einem Web App Manifest, Icons, einem Service Worker und einem Layout, das auch ohne die Bedienleisten des Browsers gut aussieht. Der offizielle Guide deckt das Manifest und Push-Benachrichtigungen ab. Viele Tutorials installieren danach `next-pwa`, ein Webpack-Plugin, in ein Framework, das inzwischen standardmäßig mit Turbopack baut. Dieses Tutorial nimmt für Next.js 16 einen schlankeren Weg: einen selbst geschriebenen Worker, der eine Offline-Seite zeigt, einen Install Prompt und eine App Shell mit Top Bar und Bottom Tabs, die Notch und Home-Indikator freihalten.

## Was braucht eine Next.js PWA im Jahr 2026?

Weniger, als ältere Guides vermuten lassen. Hier ist die komplette Liste für eine Next.js PWA mit dem App Router, jeweils mit dem, was dir das Teil bringt:

| Teil | Datei | Für die Installation nötig? | Was es dir bringt |
| --- | --- | --- | --- |
| Web App Manifest | `app/manifest.ts` | ja | Name, Icons, `standalone`-Display, Start-URL |
| Icons | `public/icon-192.png`, `public/icon-512.png`, `app/apple-icon.png` | ja | Icons auf dem Home-Bildschirm und im App-Umschalter |
| HTTPS | dein Hosting | ja (localhost ist ausgenommen) | einen sicheren Kontext für den Worker |
| Service Worker | `public/sw.js` + eine Register-Komponente | nein | eine Offline-Seite, Push, das automatische Install-Event von Chrome |
| Viewport und Theme | `export const viewport` in `app/layout.tsx` | nein | Darstellung bis an den Rand, eine farbige Statusleiste |
| Install Prompt | eine Client-Komponente im Root-Layout | nein | deinen eigenen Installieren-Button und die Schritte fürs iPhone |
| App Shell | `app/(app)/layout.tsx` | nein | Top Bar, Bottom Tabs, Safe Areas |

Die Regeln zur Installierbarkeit pro Browser, auch warum Chrome sein Install-Event beim ersten Besuch zurückhalten kann, stehen in unserem Guide zum [React PWA Install Prompt](https://wingo-ui.com/de/blog/react-pwa-install-prompt). Dieser Beitrag wiederholt sie deshalb nicht.

## Wie fügst du im App Router ein Web App Manifest hinzu?

Über die Dateikonvention `manifest`. Eine `app/manifest.ts`, die eine Funktion als Default exportiert, wird unter `/manifest.webmanifest` ausgeliefert und automatisch im `<head>` jeder Seite verlinkt:

```ts
// app/manifest.ts
import type { MetadataRoute } from "next";

export default function manifest(): MetadataRoute.Manifest {
  return {
    id: "/",
    name: "Northwind CRM",
    short_name: "Northwind",
    description: "Kunden, Angebote und Rechnungen auf deinem Smartphone.",
    start_url: "/",
    scope: "/",
    display: "standalone",
    background_color: "#ffffff",
    theme_color: "#ffffff",
    icons: [
      { src: "/icon-192.png", sizes: "192x192", type: "image/png" },
      { src: "/icon-512.png", sizes: "512x512", type: "image/png" },
      { src: "/icon-maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
    ],
  };
}
```

Leg die drei PNGs in `public/`. Beim maskierbaren Icon bleibt das Logo innerhalb der mittleren 80%, damit Android es zu einem Kreis oder einem Squircle zuschneiden kann. Fürs iPhone legst du eine `app/apple-icon.png` mit 180x180 neben das Layout; Next.js macht daraus den Link `apple-touch-icon`.

Dann sagst du dem Browser, wie die installierte App dargestellt werden soll. `viewport` und `metadata` sind im App Router getrennte Exporte, und beide funktionieren nur in Server Components:

```tsx
// app/layout.tsx
import type { Metadata, Viewport } from "next";
import type { ReactNode } from "react";
import { RegisterServiceWorker } from "./register-sw";
import "./globals.css";

export const metadata: Metadata = {
  title: "Northwind CRM",
  appleWebApp: { capable: true, title: "Northwind", statusBarStyle: "black-translucent" },
};

export const viewport: Viewport = {
  // die seite darf unter notch und home-indikator zeichnen; die shell gleicht das mit padding wieder aus
  viewportFit: "cover",
  themeColor: [
    { media: "(prefers-color-scheme: light)", color: "#ffffff" },
    { media: "(prefers-color-scheme: dark)", color: "#0b0b0f" },
  ],
};

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="de">
      <body>
        {children}
        <RegisterServiceWorker />
      </body>
    </html>
  );
}
```

`viewportFit: "cover"` ist wichtiger, als es aussieht. Ohne diese Einstellung zeigt iOS die Seite mit Rändern oben und unten, und `env(safe-area-inset-*)` bleibt 0. Komponenten, die die Safe Areas per Padding freihalten, laufen dann ins Leere. Mit der Einstellung läuft die Seite unter der Statusleiste und dem Home-Indikator durch, und Header und Tab Bar müssen diesen Platz wieder hinzufügen. Genau das macht die Shell weiter unten.

Ein Haken beim iPhone: `statusBarStyle: "black-translucent"` sorgt dafür, dass die installierte App unter der Statusleiste zeichnen darf, lässt Uhrzeit und Akkuanzeige aber weiß. Gib der Top Bar im Light Mode deshalb eine ausreichend dunkle Füllung, oder nimm `"default"` für eine undurchsichtige Statusleiste über der Seite. Der obere Inset ist dann einfach 0.

## Brauchst du für eine Next.js 16 PWA noch next-pwa?

Nein. Die letzte Version des ursprünglichen Plugins [next-pwa](https://www.npmjs.com/package/next-pwa), 5.6.0, ist im August 2022 erschienen, und der gepflegte Fork [@ducanh2912/next-pwa](https://www.npmjs.com/package/@ducanh2912/next-pwa) hat zuletzt im September 2024 die Version 10.2.9 veröffentlicht (npm-Registry, geprüft am 5. November 2026). Beide hängen sich in Webpack ein, während Next.js 16 `next dev` und `next build` mit Turbopack ausführt, solange du nicht `--webpack` übergibst. Laut [Upgrade-Guide für Next.js 16](https://nextjs.org/docs/app/guides/upgrading/version-16) schlägt `next build` fehl, wenn es eine Webpack-Konfiguration findet, und auch ein Plugin, das eine hinzufügt, zählt dazu. Eine Next.js 16 PWA auf Basis von next-pwa bleibt also entweder mit `--webpack` bei Webpack oder wird mit `next build --turbopack`, das die Konfiguration ignoriert, ohne generierten Worker ausgeliefert.

Zwei Optionen funktionieren mit Turbopack:

- **Ein selbst geschriebener Worker** für eine Offline-Seite und Push. Das ist eine einzige Datei in `public/`, ohne Build-Schritt und ohne Abhängigkeit. Den baut dieser Beitrag.
- **[Serwist](https://serwist.pages.dev/docs/next/turbo)**, wenn du Routen vorab cachen und Assets mit Strategien im Stil von Workbox cachen willst. Das Paket `@serwist/turbopack` (9.5.13, 4. Oktober 2026) liefert den Worker über einen Route Handler aus, der mit `createSerwistRoute` gebaut wird, kompiliert `app/sw.ts` mit esbuild und legt einen `SerwistProvider` um den Baum. Auch der [Next.js-Guide zu PWAs](https://nextjs.org/docs/app/guides/progressive-web-apps) verweist für vollständiges Offline-Caching auf Serwist.

Fang mit dem selbst geschriebenen Worker an. Steig erst an dem Tag auf Serwist um, an dem du gecachte Daten-Screens brauchst, nicht früher, denn jedes gecachte Asset ist eine Version, die du irgendwann wieder ablaufen lassen musst.

## Wie zeigst du in einer Next.js PWA eine Offline-Seite?

Fang gescheiterte Seitenaufrufe im Worker ab und beantworte sie mit einer Seite, die du bei der Installation gecacht hast. Das ist das web.dev-Rezept für eine [Offline-Fallback-Seite](https://web.dev/articles/offline-fallback-page), angepasst an Next.js. Es ist das schlankste Next.js PWA Offline-Setup, das sich trotzdem wie eine App anfühlt statt wie der Dinosaurier des Browsers.

### 1. Eine eigenständige Offline-Seite

Mach die Offline-Seite nicht zu einer Next.js-Route. Ihr CSS und JavaScript liegen in gehashten Dateien unter `/_next/static`, die dein Worker nie gecacht hat, also würde sie offline als HTML ohne Styles laden. Schreib stattdessen einfaches HTML mit Inline-Styles:

```html
<!-- public/offline.html -->
<!doctype html>
<html lang="de">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
    <title>Du bist offline</title>
    <style>
      :root { color-scheme: light dark; --bg: #ffffff; --fg: #0b0b0f; --muted: #5b5b66; }
      @media (prefers-color-scheme: dark) { :root { --bg: #0b0b0f; --fg: #f5f5f7; --muted: #a1a1aa; } }
      body { margin: 0; min-height: 100dvh; display: grid; place-items: center; background: var(--bg); color: var(--fg);
        font: 16px/1.5 system-ui, sans-serif; padding: env(safe-area-inset-top) 24px env(safe-area-inset-bottom); }
      main { max-width: 22rem; text-align: center; }
      p { color: var(--muted); }
      button { min-height: 44px; padding: 0 20px; border: 0; border-radius: 12px; background: var(--fg); color: var(--bg);
        font: inherit; font-weight: 600; touch-action: manipulation; }
    </style>
  </head>
  <body>
    <main>
      <h1>Du bist offline</h1>
      <p>Northwind braucht eine Verbindung, um diese Seite zu laden. Sobald du wieder online bist, lädt sie von selbst neu.</p>
      <button type="button" onclick="location.reload()">Erneut versuchen</button>
    </main>
    <script>
      // lädt von selbst neu, sobald die verbindung zurück ist
      addEventListener("online", () => location.reload());
    </script>
  </body>
</html>
```

### 2. Der Service Worker

```js
// public/sw.js
const CACHE = "offline-v1";
const OFFLINE_URL = "/offline.html";

self.addEventListener("install", (event) => {
  // cache: "reload" umgeht den http-cache, die seite kommt also frisch vom server
  event.waitUntil(caches.open(CACHE).then((cache) => cache.add(new Request(OFFLINE_URL, { cache: "reload" }))));
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      if (self.registration.navigationPreload) await self.registration.navigationPreload.enable();
      const keys = await caches.keys();
      await Promise.all(keys.filter((key) => key !== CACHE).map((key) => caches.delete(key)));
      await self.clients.claim();
    })(),
  );
});

self.addEventListener("fetch", (event) => {
  // nur komplette seitenaufrufe; rsc-payloads, bilder und api-aufrufe gehen direkt ans netz
  if (event.request.mode !== "navigate") return;
  event.respondWith(
    (async () => {
      try {
        const preloaded = await event.preloadResponse;
        if (preloaded) return preloaded;
        return await fetch(event.request);
      } catch {
        // ein netzwerkfehler, kein 404 oder 500: die kommen weiterhin bei der seite an
        const cache = await caches.open(CACHE);
        return (await cache.match(OFFLINE_URL)) ?? Response.error();
      }
    })(),
  );
});
```

`skipWaiting()` und `clients.claim()` sind hier unbedenklich, weil dieser Worker keinen App-Code ausliefert. Sobald ein Worker deine JavaScript-Chunks cacht, kann ein neuer Worker, der unter einem offenen Tab aktiv wird, alte und neue Dateien mischen, und dann brauchst du stattdessen einen Hinweis „Update verfügbar“. Erhöh `CACHE` auf `offline-v2`, sobald sich `offline.html` ändert; der activate-Schritt löscht den alten Cache.

### 3. Registrier ihn aus einer Client-Komponente

```tsx
// app/register-sw.tsx
"use client";

import { useEffect } from "react";

export function RegisterServiceWorker() {
  useEffect(() => {
    // dev baut ständig neu; teste den worker mit next build && next start
    if (process.env.NODE_ENV !== "production" || !("serviceWorker" in navigator)) return;
    navigator.serviceWorker.register("/sw.js", { scope: "/", updateViaCache: "none" }).catch(() => {});
  }, []);
  return null;
}
```

`updateViaCache: "none"` sorgt dafür, dass die Update-Prüfung bei Navigationen `sw.js` vom Server holt statt aus dem HTTP-Cache. Sichere das mit einem Header ab, damit auch CDNs keinen alten Worker vorhalten:

```ts
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/sw.js",
        headers: [
          { key: "Cache-Control", value: "no-cache, no-store, must-revalidate" },
          { key: "Content-Security-Policy", value: "default-src 'self'; script-src 'self'" },
        ],
      },
    ];
  },
};

export default nextConfig;
```

### 4. Teste ihn

Führ `next build && next start` aus und öffne `http://localhost:3000`. localhost ist ein sicherer Kontext, du brauchst also kein Zertifikat. In den Chrome DevTools listet Application das Manifest und den aktiven Worker auf. Stell das Network-Panel auf Offline und lad neu: Statt der Fehlerseite des Browsers bekommst du deine eigene Seite.

## Was passiert mit clientseitigen Navigationen, wenn das Netz wegfällt?

Sie scheitern wie jede andere Anfrage, denn der Worker oben sieht nur komplette Seitenaufrufe. Ein Tipp auf einen `<Link>` holt per `fetch()` eine Payload für React Server Components, keine Navigationsanfrage, also lässt der Worker sie zum Netz durch. Next.js 16 hat für diese Ebene eine Lösung, die noch als experimentell markiert ist: Mit `experimental.useOffline: true` in `next.config.ts` bleibt eine gescheiterte Navigation, ein Prefetch oder eine Server Action ausstehend und wird wiederholt, sobald die Verbindung zurück ist, und der Hook `useOffline()` aus `next/offline` sagt deiner UI, dass sie offline ist. Der [Guide zum Offline-Support](https://nextjs.org/docs/app/guides/offline-support) zeigt darauf aufbauend ein Banner im Root-Layout. Betrachte das vorerst als nettes Extra und behalte den Worker als Sicherheitsnetz, denn ein experimentelles Flag kann sich zwischen Minor-Versionen ändern.

## Wie fügst du einer Next.js PWA einen Install Prompt hinzu?

Binde die Komponente im Root-Layout ein, denn Chrome feuert `beforeinstallprompt` früh im Seitenaufbau, und eine Komponente, die erst später auf einer einzelnen Route mountet, kann das Event verpassen. Alle Details, von `beforeinstallprompt` über das Teilen-Menü auf dem iPhone bis zu der Frage, wann du besser nicht fragst, steht in [React PWA Install Prompt für Chrome, Edge und iPhone](https://wingo-ui.com/de/blog/react-pwa-install-prompt). Die Kurzfassung mit unserem [Install Prompt](https://wingo-ui.com/components/install-prompt):

```tsx
// in app/layout.tsx, neben <RegisterServiceWorker />
import { InstallPrompt } from "@/components/ui/install-prompt";

<InstallPrompt
  appName="Northwind CRM"
  appIcon="/icon-192.png"
  description="Öffne sie direkt vom Home-Bildschirm, auch offline."
/>
```

In Chrome und Edge zeigt er den Installationsdialog des Browsers, auf dem iPhone die Schritte für „Zum Home-Bildschirm“. In der installierten App und in In-App-Browsern bleibt er verborgen, und ein Wegklicken merkt er sich standardmäßig 14 Tage lang. Auf dem Smartphone sitzt er über der Bottom Tab Bar aus dem nächsten Abschnitt, weil er deren `--bottom-bar-offset` ausliest. Die Offline-Seite aus dem vorigen Abschnitt macht „auch offline“ zu einem ehrlichen Versprechen.

## Wie fühlt sich eine Next.js PWA auf dem Smartphone nativ an?

Gib der installierten App ihre eigenen Bedienleisten. Im Modus `standalone` gibt es keine Adressleiste und keinen Zurück-Button, also muss die App einen Header, einen Weg zurück und eine Hauptnavigation in Reichweite des Daumens mitbringen. Auf dem Smartphone heißt das: eine Top Bar und eine Bottom Tab Bar, beide mit Abstand für die Safe Areas, die du mit `viewportFit: "cover"` geöffnet hast.

Die [App Shell](https://wingo-ui.com/components/app-shell) erledigt das als ein einziges Layout: Sidebar und Top Bar auf dem Desktop, eine Drawer-Navigation und eine [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) unter 768px. Zieh die Demo unten auf Smartphone-Breite zusammen oder öffne sie auf deinem Smartphone und wechsle unten die Tabs:

> Live-Demo (App Shell): Dieselbe Shell auf Desktop und Smartphone: Mach das Fenster schmaler als 768px, dann wird die Sidebar zu einer Drawer-Navigation hinter dem Menü-Button, mit Tabs am unteren Rand. Probier sie unter [App Shell](https://wingo-ui.com/components/app-shell) aus und installier sie mit `npx wingo-ui@latest add app-shell`.

Installier sie mit `npx wingo-ui@latest add app-shell`; das kopiert auch Sidebar, Topbar und Bottom Tab Bar mit, die sie importiert. Dann machst du sie zum Layout der Routen für angemeldete Nutzer. Zuerst ein Provider, damit jeder Tab und jeder Sidebar-Link über `next/link` gerendert wird:

```tsx
// app/providers.tsx
"use client";

import Link from "next/link";
import type { ReactNode } from "react";
import { usePathname, useRouter } from "next/navigation";
import { LinkProvider } from "@/lib/navigation";

export function Providers({ children }: { children: ReactNode }) {
  const router = useRouter();
  return (
    <LinkProvider component={Link} currentPath={usePathname()} navigate={router.push}>
      {children}
    </LinkProvider>
  );
}
```

Pack `{children}` im Root-Layout in `<Providers>` und bau dann die Shell:

```tsx
// app/(app)/layout.tsx
import type { ReactNode } from "react";
import { FileText, House, Inbox, Settings, Users } from "lucide-react";
import { AppShell } from "@/components/ui/app-shell";
import type { SidebarNavGroup } from "@/components/ui/sidebar";
import { Topbar } from "@/components/ui/topbar";
import type { NavItem } from "@/lib/navigation";

const nav: SidebarNavGroup[] = [
  {
    items: [
      { label: "Start", href: "/", icon: <House /> },
      { label: "Posteingang", href: "/inbox", icon: <Inbox />, badge: 4 },
    ],
  },
  {
    label: "Vertrieb",
    items: [
      { label: "Kunden", href: "/clients", icon: <Users /> },
      { label: "Rechnungen", href: "/invoices", icon: <FileText /> },
    ],
  },
  { label: "Konto", items: [{ label: "Einstellungen", href: "/settings", icon: <Settings /> }] },
];

// vier tabs: die wichtigsten ziele auf dem smartphone
const tabs: NavItem[] = [
  { label: "Start", href: "/", icon: <House /> },
  { label: "Kunden", href: "/clients", icon: <Users /> },
  { label: "Posteingang", href: "/inbox", icon: <Inbox />, badge: 4 },
  { label: "Einstellungen", href: "/settings", icon: <Settings /> },
];

export default function AppLayout({ children }: { children: ReactNode }) {
  return (
    <AppShell nav={nav} topbar={<Topbar title="Northwind" />} tabs={tabs}>
      {children}
    </AppShell>
  );
}
```

Drei Details sorgen dafür, dass sie sich installiert anfühlt und nicht wie eine eingebettete Webseite:

- **Safe Areas.** Die Topbar rechnet `env(safe-area-inset-top)` als Padding ein, die Bottom Tab Bar `env(safe-area-inset-bottom)`, beides standardmäßig aktiv (`safeArea`). Unser Beitrag zu [Tailwind Safe Area Insets](https://wingo-ui.com/de/blog/tailwind-safe-area-insets) deckt den Rest der Seite ab.
- **Echte Viewport-Höhe.** Die Shell bemisst sich mit `dvh` statt `vh`, also endet eine kurze Seite am sichtbaren unteren Rand und nicht hinter der Toolbar von Safari. Die Hintergründe stehen in [100vh auf dem Smartphone und dvh in Tailwind](https://wingo-ui.com/de/blog/100vh-mobile-dvh-tailwind).
- **Scrollen im Fenster.** Mit dem Standard `scroll="window"` funktioniert die Wiederherstellung der Scrollposition durch den Router weiter, und die fixierte Tab Bar schreibt `--bottom-bar-offset`, damit schwebende Buttons ihr ausweichen.

### Blende reine Browser-UI in der installierten App aus

Ein Marketing-Footer oder ein Link „App holen“ wirkt in der App selbst seltsam. Ergänz in `app/globals.css` eine Variante für den Display-Modus in Tailwind v4:

```css
@custom-variant standalone (@media (display-mode: standalone));
```

Dann entfernt `className="standalone:hidden"` ein Element nur in der installierten App, und `standalone:pb-4` kann Platz hinzufügen, der nur dort zählt. Es ist CSS, also stimmt es schon beim ersten Paint, ohne Flackern bei der Hydration.

### Starte mit einem fertigen Home-Screen

Für eine Consumer-App ist der Block [Mobile App Home](https://wingo-ui.com/components/mobile-app-home) ein kompletter erster Screen auf Basis derselben Tab Bar: ein großer Titel, der beim Scrollen in die Leiste schrumpft, Suche, Stories, Schnellaktionen, Feed-Tabs und Pull to Refresh. Scroll ihn, zieh oben nach unten und wechsle die Feed-Tabs:

> Live-Demo (Mobile App Home): Ein Marktplatz-Home-Screen, wie ihn eine installierte PWA zeigen würde: Scroll, damit der Titel schrumpft, zieh nach unten zum Aktualisieren und tipp unten auf die Tabs. Probier sie unter [Mobile App Home](https://wingo-ui.com/components/mobile-app-home) aus und installier sie mit `npx wingo-ui@latest add mobile-app-home`.

Mit der Prop `desktop="full"` streckt er sich auf dem Tablet über die volle Breite, statt eine zentrierte Spalte mit 440px zu behalten. Das passt zu einer installierten App auf dem iPad.

## Was solltest du vor dem Release der PWA prüfen?

Sechs Checks, wo möglich auf echten Geräten, denn die DevTools zeigen dir weder die Notch noch das Icon im Launcher noch den Flugmodus:

1. **Manifest.** In den DevTools zeigt Application, dann Manifest keine Fehler, und beide Icon-Größen laden.
2. **Offline.** Installier die App, schalte den Flugmodus ein und öffne sie: Du siehst `offline.html`, und sie lädt neu, sobald die Verbindung zurück ist.
3. **iPhone.** Füg sie in Safari zum Home-Bildschirm hinzu. Der Text in der Statusleiste ist über deiner Top Bar lesbar, nichts liegt unter der Notch, und die Tab Bar hält Abstand zum Home-Indikator.
4. **Android.** Installier sie aus Chrome, prüf das maskierbare Icon im Launcher und dreh das Smartphone einmal.
5. **Updates.** Ändere `offline.html`, erhöh `CACHE`, deploye und öffne die App zweimal: Die neue Seite erscheint, und die DevTools listen einen einzigen Cache.
6. **Desktop.** Installier sie aus Chrome oder Edge; die Shell sollte im eigenen Fenster das Layout mit Sidebar zeigen.

Wenn du eigentlich nach einem Nextjs PWA Template gesucht hast, lass diese Checkliste darüber laufen, bevor du es übernimmst. Schritt 3 ist der, den dir ein Desktop-Browser nicht abnehmen kann.

## Was solltest du als Nächstes bauen?

Liefer zuerst die vier Dateien aus: `app/manifest.ts`, `public/offline.html`, `public/sw.js` und die Register-Komponente. Sie sind reines Next.js, kostenlos und geben dir eine installierbare App mit einem ehrlichen Offline-Zustand. Für die Schicht der installierten App kopiert `npx wingo-ui@latest add app-shell install-prompt` die Shell, die Bottom Tabs und das Installationsbanner als Quellcode in dein Projekt; sie gehören zu [Wingo UI Pro](https://wingo-ui.com/de/pricing) ($8 im Monat, $80 im Jahr oder $150 einmalig). Mehr Muster für Apps, die man mit einer Hand hält, findest du im Pillar-Beitrag zu [Mobile-first React-Komponenten](https://wingo-ui.com/de/blog/mobile-first-react-components) und in der Kategorie [Mobile UI in React](https://wingo-ui.com/de/blog/category/mobile-ui), und [React Native vs PWA](https://wingo-ui.com/de/blog/react-native-vs-pwa) hilft dir, falls du noch abwägst, ob das Web reicht.

## Komponenten in diesem Artikel

- [Install Prompt](https://wingo-ui.com/components/install-prompt) (Pro): A React PWA install prompt: the browser's install dialog in Chrome and Edge, and an Add to Home Screen guide on iPhone and Mac. Installation: `npx wingo-ui@latest add install-prompt`
- [App Shell](https://wingo-ui.com/components/app-shell) (Pro): A React app shell for admin layouts: a collapsible, resizable sidebar, a top bar and the page, with a drawer and a tab bar on phones. Installation: `npx wingo-ui@latest add app-shell`
- [Bottom Tab Bar](https://wingo-ui.com/components/bottom-tab-bar) (Pro): A React bottom tab bar for phones: 3 to 5 tabs at the thumb with an animated pill, badges, a raised center action and hide on scroll. Installation: `npx wingo-ui@latest add bottom-tab-bar`
- [Mobile App Home](https://wingo-ui.com/components/mobile-app-home) (Pro): A React mobile app home screen for a marketplace: large title, search, stories, quick actions, recommended ads, feed tabs and a tab bar. Installation: `npx wingo-ui@latest add mobile-app-home`

## Häufige Fragen

### Funktioniert next-pwa mit Next.js 16?

Nicht ohne Weiteres. Das ursprüngliche Paket next-pwa ist ein Webpack-Plugin, dessen letzte Version 5.6.0 aus dem August 2022 stammt, und Next.js 16 baut mit Turbopack. next build bricht deshalb an der Webpack-Konfiguration ab, die das Plugin hinzufügt, solange du nicht --webpack übergibst. Nimm für eine Offline-Seite einen selbst geschriebenen Service Worker, oder Serwist, dessen Paket @serwist/turbopack (9.5.13, Oktober 2026) in der Doku ein Setup für Turbopack hat.

### Braucht eine Next.js PWA einen Service Worker, um installierbar zu sein?

In aktuellen Chromium-Browsern und auf dem iPhone nicht: Ein Manifest mit name, Icons in 192px und 512px, start_url und standalone-Display, über HTTPS ausgeliefert, reicht für die Installation aus dem Browsermenü. Für eine Offline-Seite, Push-Benachrichtigungen und das automatische beforeinstallprompt-Event in Chrome willst du trotzdem einen.

### Warum erscheint meine Offline-Seite in Next.js ohne Styles?

CSS und JavaScript der Seite liegen in gehashten Dateien unter /_next/static, die dein Worker nie gecacht hat. Offline lädt also das HTML, das Stylesheet aber nicht. Halte die Offline-Seite eigenständig, mit Inline-CSS in public/offline.html, so wie es das Offline-Fallback-Rezept von web.dev vorsieht.

### Wie teste ich einen Service Worker in Next.js lokal?

Führ next build und next start aus und öffne http://localhost:3000. localhost gilt als sicherer Kontext, der Worker registriert sich also ohne HTTPS. In den Chrome DevTools zeigt Application das Manifest und den Worker, und mit der Option Offline im Network-Panel lädst du die Seite neu und landest auf der Offline-Seite.

### Gibt es ein Next.js PWA Template mit App Shell?

App Shell, Bottom Tab Bar, Install Prompt und der Block Mobile App Home von Wingo UI liefern die Schicht für die installierte App: Top Bar, Bottom Tabs mit Safe Areas, eine Drawer-Navigation auf dem Smartphone und ein Installationsbanner. Sie gehören zu Pro und werden mit npx wingo-ui@latest add als Quellcode installiert.

---

Quelle: https://wingo-ui.com/de/blog/nextjs-pwa
