Skip to content
On this page Manifest

Getting started

PWA

Make a SvelteKit app installable with a themed web app manifest, iOS home screen tags and a service worker that keeps navigations on the network and falls back to an offline page.

A progressive web app needs three pieces: a manifest (name, icons, colors), the iOS tags that Safari still reads instead of the manifest, and a service worker. @codeatlasdev/ui/seo generates the first two. The service worker stays in your app on purpose: its cache rules depend on your routes and release flow.

Manifest

manifest() returns the manifest object. theme_color and background_color come from the same theme definition you give caThemeHandle: they are the surface color of the default preset in the default scheme (the dark one unless defaultScheme is "light"), the value the init script puts in <meta name="theme-color">. The splash screen and the browser chrome therefore match the first paint.

// src/routes/manifest.webmanifest/+server.ts
import { manifest } from "@codeatlasdev/ui/seo";
import { theme } from "$lib/theme"; // SvelteKit 3: "#lib/theme.ts"

export const prerender = true;

export const GET = () =>
  new Response(
    JSON.stringify(
      manifest({
        name: "Acme Seller",
        shortName: "Seller",
        description: "Orders and customers on the go.",
        theme,
        icons: [
          { src: "/icons/192.png", sizes: "192x192" },
          { src: "/icons/512.png", sizes: "512x512" },
          { src: "/icons/maskable-512.png", sizes: "512x512", purpose: "maskable" },
        ],
        shortcuts: [{ name: "New order", url: "/orders/new" }],
      }),
    ),
    { headers: { "content-type": "application/manifest+json" } },
  );

Defaults are display: "standalone", start_url: "/", scope: "/" and short_name equal to name. Each icon gets type: "image/png" unless you set one. Pass themeColor or backgroundColor (as #rrggbb) to override the derived colors, and extra for members the builder does not cover (categories, screenshots, id).

Link it from the page head with <Seo manifest="/manifest.webmanifest" ... />.

iOS home screen

Safari ignores most manifest fields. appleMeta() returns the tags it does read, as an HTML string:

<script>
  import { appleMeta } from "@codeatlasdev/ui/seo";
</script>

<svelte:head>
  {@html appleMeta({ touchIcon: "/icons/apple-touch-180.png", title: "Seller" })}
</svelte:head>
Tag Effect
mobile-web-app-capable and apple-mobile-web-app-capable Opens without browser chrome.
apple-mobile-web-app-status-bar-style black-translucent (default), default or black. The default draws the page under the status bar: it needs viewport-fit=cover; pad with var(--ca-safe-top).
apple-mobile-web-app-title The label under the icon, emitted when you pass title.
apple-touch-icon 180x180 PNG, from touchIcon. Use an opaque image: iOS fills transparency with black.

Service worker

SvelteKit builds the worker when src/service-worker.ts (Kit 2) or src/service-worker/index.ts (Kit 3) exists and registers it for you. bunx @codeatlasdev/ui init --write creates the right one for your Kit major, plus the prerendered /offline page. The build lists give the hashed build files, static files and the app version, so the cache never serves stale assets after a deploy.

Kit 3 removed $service-worker: self comes from $app/service-worker, the file lists from $app/manifest (immutable, assets) and version from $app/env. $app/forms, $app/navigation and $app/state cannot be imported into a worker (the build fails with service_worker_invalid_import), and tsconfig.json must list src/service-worker in exclude (the worker has its own $app/tsconfig/service-worker).

The pattern below (Kit 2 imports; the rules are the same on Kit 3) mirrors what works in production apps:

Request Strategy Why
Build files (/_app/immutable/*) and static assets Cache first Content-hashed, safe forever.
Page navigations Network first, then the cached offline page Users always see fresh data; offline shows a real page instead of the browser error.
Everything else (API, auth, form actions) Untouched Never cache authenticated data.
// src/service-worker.ts
/// <reference types="@sveltejs/kit" />
/// <reference no-default-lib="true"/>
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { build, files, version } from "$service-worker";

const sw = self as unknown as ServiceWorkerGlobalScope;
const CACHE = `app-${version}`;
const ASSETS = [...build, ...files, "/offline"];

sw.addEventListener("install", (event) => {
  event.waitUntil(
    caches
      .open(CACHE)
      .then((cache) => cache.addAll(ASSETS))
      .then(() => sw.skipWaiting()),
  );
});

sw.addEventListener("activate", (event) => {
  event.waitUntil(
    caches
      .keys()
      .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
      .then(() => sw.clients.claim()),
  );
});

sw.addEventListener("fetch", (event) => {
  const { request } = event;
  if (request.method !== "GET") return;
  const url = new URL(request.url);
  if (url.origin !== sw.location.origin) return;

  // Network first for pages, offline page when the network fails.
  if (request.mode === "navigate") {
    event.respondWith(
      fetch(request).catch(async () => (await caches.match("/offline")) ?? Response.error()),
    );
    return;
  }

  // Cache first for hashed build output and static files.
  if (ASSETS.includes(url.pathname)) {
    event.respondWith(caches.match(request).then((hit) => hit ?? fetch(request)));
  }
});

Create src/routes/offline/+page.svelte with a short message and a retry button, and set export const prerender = true; in a sibling +page.ts so it exists as static HTML when the worker installs. Using version in the cache name replaces old caches on every release.

Install prompt

Browsers only offer to install an app that meets the criteria: the manifest above (display: "standalone", 192 and 512 icons) and a service worker. InstallPrompt turns that into UI: put it once in the root layout and it shows the native install dialog on Chromium, the Share, Add to Home Screen steps on iOS, and nothing once the app is installed or the user dismissed it.

Checklist

Check Where
Manifest linked and valid in DevTools, Application, Manifest Seo manifest
Maskable icon and 512px icon present manifest()
viewport-fit=cover in the viewport meta app.html
iOS tags present appleMeta()
Offline page prerendered and cached on install src/service-worker.ts
No authenticated responses cached fetch handler
An install entry point InstallPrompt in the root layout