PWA

@o/pipeline/pwa keeps installation metadata and device descriptors framework-free. createPWA() binds Pipeline’s shared generated descriptors to one app’s identity. The Node-only renderer runs from pnpm --filter @o/pipeline pwa:build; it may use React SSR and Sharp, but neither reaches the browser metadata entry.

Configuration

Each app owns its installed identity, launch URL, Apple status-bar choice, and optional legacy icon fallback. display defaults to standalone, which is what both current apps want; set it only to opt out of an app-like window. Note that minimal-ui is not a free choice on iOS: startup images need apple-mobile-web-app-capable, and that tag runs the app standalone regardless of what the manifest asks for, so declaring minimal-ui alongside it asks for two different things. On macOS Add to Dock the mode is honoured, where standalone and fullscreen hide the toolbar while minimal-ui and browser keep one. Shared image descriptors are bound inside createPWA(), so an app name or manifest-only change does not produce a new image URL.

import { createPWA } from "@o/pipeline/pwa";

export const pwa = createPWA({
  app: {
    apple: { capable: true, statusBarStyle: "default" },
    description: "A documented app.",
    id: "/",
    name: "Example",
    origin: "https://example.com",
    scope: "/",
    shortName: "Example",
    startUrl: "/",
  },
});

export const { manifest, icons, themeColorScript } = pwa;

Head and manifest data

Use the returned data to adapt a framework’s head API. pwa.head() resolves a document title, metadata, and links in one result. title, description, canonicalUrl, origin, and socialTitle are page or request overrides. socialTitle is useful when a framework’s document title includes the app name but its existing Open Graph title should remain page-specific.

import { pwa } from "./pwa.config";

const head = pwa.head({
  canonicalUrl: "https://example.com/reference",
  description: "Reference documentation.",
  socialTitle: "Reference",
  title: "Reference | Example",
});

Operate adapts head.title, head.meta, and head.links to TanStack Start. Design adapts the same data to Astro. Both keep their existing theme controller as the runtime owner; the shared theme-color script only follows the controller’s data-theme result.

Generated assets

Icons and social image

Green Operate mark on a light canvas.
Favicon fallback
favicon-32.png
Green Operate mark on a light canvas.
Home Screen icon
icon-180.png
Green Operate mark on a light canvas.
Scalable icon
icon.svg
Green Operate mark on a light canvas.
Standard icon
icon-192.png
Green Operate mark on a light canvas.
Standard icon
icon-512.png
Green Operate mark on a light canvas.
Standard icon
icon-1024.png
Green Operate mark on a light canvas.
Maskable icon
icon-maskable-192.png
Green Operate mark on a light canvas.
Maskable icon
icon-maskable-512.png
Green Operate mark on a light canvas.
Maskable icon
icon-maskable-1024.png
Light gray Operate mark above an italic Operate wordmark on a green canvas.
Shared Open Graph image
og.png

iOS startup images

iPhone SE (1st generation)

320 × 568 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-320x568@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-320x568@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-320x568@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-320x568@2-landscape.png

iPhone 6, 6s, 7, and 8, iPhone SE (2nd and 3rd generation)

375 × 667 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-375x667@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-375x667@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-375x667@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-375x667@2-landscape.png

iPhone 6 Plus, 6s Plus, 7 Plus, and 8 Plus

414 × 736 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-414x736@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-414x736@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-414x736@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-414x736@3-landscape.png

iPhone X, XS, 11 Pro, 12 mini, and 13 mini

375 × 812 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-375x812@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-375x812@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-375x812@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-375x812@3-landscape.png

iPhone XR and 11

414 × 896 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-414x896@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-414x896@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-414x896@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-414x896@2-landscape.png

iPhone XS Max and 11 Pro Max

414 × 896 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-414x896@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-414x896@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-414x896@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-414x896@3-landscape.png

iPhone 12, 12 Pro, 13, 13 Pro, and 14

390 × 844 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-390x844@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-390x844@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-390x844@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-390x844@3-landscape.png

iPhone 12 Pro Max, 13 Pro Max, and 14 Plus

428 × 926 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-428x926@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-428x926@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-428x926@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-428x926@3-landscape.png

iPhone 14 Pro, iPhone 15, iPhone 15 Pro, iPhone 16

393 × 852 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-393x852@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-393x852@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-393x852@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-393x852@3-landscape.png

iPhone 14 Pro Max, iPhone 15 Plus, iPhone 15 Pro Max, iPhone 16 Plus

430 × 932 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-430x932@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-430x932@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-430x932@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-430x932@3-landscape.png

iPhone 16 Pro

402 × 874 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-402x874@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-402x874@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-402x874@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-402x874@3-landscape.png

iPhone 16 Pro Max

440 × 956 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-440x956@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-440x956@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-440x956@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-440x956@3-landscape.png

iPhone Air

420 × 912 CSS px at 3×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-420x912@3-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-420x912@3-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-420x912@3-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-420x912@3-landscape.png

Retina iPad and iPad mini

768 × 1024 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-768x1024@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-768x1024@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-768x1024@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-768x1024@2-landscape.png

iPad (7th through 9th generation)

810 × 1080 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-810x1080@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-810x1080@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-810x1080@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-810x1080@2-landscape.png

iPad Air (4th and 5th generation), iPad Air 11-inch (M2 through M4), and iPad (10th generation and A16)

820 × 1180 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-820x1180@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-820x1180@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-820x1180@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-820x1180@2-landscape.png

iPad mini (6th generation and A17 Pro)

744 × 1133 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-744x1133@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-744x1133@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-744x1133@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-744x1133@2-landscape.png

iPad Pro 10.5-inch and iPad Air (3rd generation)

834 × 1112 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-834x1112@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-834x1112@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-834x1112@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-834x1112@2-landscape.png

iPad Pro 11-inch

834 × 1194 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-834x1194@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-834x1194@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-834x1194@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-834x1194@2-landscape.png

iPad Pro 11-inch (M4)

834 × 1210 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-834x1210@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-834x1210@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-834x1210@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-834x1210@2-landscape.png

iPad Pro 12.9-inch and iPad Air 13-inch (M2 through M4)

1024 × 1366 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-1024x1366@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-1024x1366@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-1024x1366@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-1024x1366@2-landscape.png

iPad Pro 13-inch (M4)

1032 × 1376 CSS px at 2×

Green Operate mark on a light portrait startup canvas.
light portrait
splash-light-1032x1376@2-portrait.png
Green Operate mark on a light landscape startup canvas.
light landscape
splash-light-1032x1376@2-landscape.png
Green Operate mark on a dark portrait startup canvas.
dark portrait
splash-dark-1032x1376@2-portrait.png
Green Operate mark on a dark landscape startup canvas.
dark landscape
splash-dark-1032x1376@2-landscape.png

The renderer writes deployment artwork only to packages/pipeline/public/pwa/.generated/<hash>/, then writes its compact build facts to src/pwa/.generated/hash.ts. Existing Pipeline bootstrap copies the public collection into each consumer at /.pipeline/pwa/.generated/<hash>/. Each app owns its framework manifest endpoint at /manifest.webmanifest; it derives that document from its own pwa output without rendering images.

The hash is SHA-256 over codepoint-sorted logical filenames and encoded image bytes. It excludes app identity, timestamps, absolute paths, and generated descriptor facts. The runtime asset map derives URLs and descriptors from the shared icon and device specifications, with the generated hash and measured Open Graph dimensions as its only build facts.

Run these commands after changing a shared rendering input, device descriptor, or the shared Open Graph source:

pnpm --filter @o/pipeline pwa:build
pnpm --filter @o/pipeline pwa:check

Generation is explicit. It does not run from lint-staged, install, prepare, normal builds, or app metadata changes. Pipeline’s test command runs the read-only check before Vitest, so a forgotten regeneration fails without mutating the checkout. Stage regenerated shared outputs with their source changes.

Serve hash-addressed image files with Cache-Control: public, max-age=31536000, immutable. Each app’s /manifest.webmanifest must revalidate because its document text can change without a new image hash.

Only the current hash directory is retained. A page or installed app that still points at a deleted directory can show a missing asset until it reloads.

iOS snapshots the Home Screen icon and startup images when the app is added. Nothing invalidates that snapshot: a new collection hash changes the URLs, but an existing installation keeps the artwork it was installed with until the user deletes the app and adds it again. Treat that as a constraint on when artwork ships rather than a refresh step, because every installation made before the final icon lands keeps the placeholder. Social-preview caches are separate, and each platform expires them on its own schedule.

The collection includes the byte-preserved 1200 by 628 Open Graph source at src/pwa/assets/og.png.

Branding assets this needs supplied

Everything currently rendered derives from one placeholder: the Operate mark in packages/pipeline/src/experimental/logos/operate.tsx, a 12 × 12 path drawn on a flat canvas. That is enough to build and test the pipeline, and it is not enough to ship. This list records what real artwork has to arrive before the placeholder comes out, and which items cannot simply be scaled from the mark.

AssetWhy it is neededCurrent placeholder
Icon-grade app mark, vectorEvery generated icon and splash derives from this one path. A UI-grade mark scaled into an icon slot reads as a logo in a box rather than an app icon.operate.tsx, a 12 × 12 path shared with in-product UI
iOS app icon, 1024 square, opaqueiOS applies its own squircle mask and shadow, so the source must be full-bleed with no transparency and no pre-rounded corners. Artwork has to be composed for that crop.Centred mark at 52% of the shorter edge on #FCFCFC
macOS app iconmacOS uses a rounded rectangle with its own inset, depth and shadow conventions. It is a different composition from the iOS square, not a re-export of it.None; icon-1024.png is offered to ToDesktop as a stand-in
Maskable iconAndroid crops to an arbitrary shape, so the mark needs a deliberate safe zone rather than the standard inset.Same mark at 40% of the shorter edge
Optical small sizes, 16 and 32A mark that reads at 1024 turns to mud at 32. Small sizes want a redrawn, simplified form, not a downscale.favicon-32.png, a straight downscale
Splash canvas and mark treatmentThe launch image is the first frame of the app. Its canvas must match the app background per appearance, and the mark’s scale on it is a composition decision.Mark at 20% of the shorter raster edge on the canvas colour
Canvas colours, light and darkThese drive the splash background, the manifest background_color and the initial theme_color, so they are brand decisions rather than incidental values.#FCFCFC light, #141414 dark, taken from --color-canvas
Mark colourApplied to every generated icon and splash.#117025
Open Graph card, 1200 × 628Shared social artwork. The current file is inherited from Design and is deliberately generic.src/pwa/assets/og.png, reused unchanged

Two constraints worth knowing before commissioning any of it.

iOS only accepts PNG manifest icons and ignores maskable, so the SVG and maskable families exist for other platforms rather than for Apple. And artwork timing matters more than it looks: iOS snapshots the Home Screen icon at install, so anyone who installs before the real icon ships keeps the placeholder until they delete and re-add the app.

Once approved artwork lands, replace the source, run the two commands above, and commit the regenerated collection. Nothing here is hand-exported per size.

iOS startup images

IOS_STARTUP_DEVICE_FAMILIES groups named Apple families by their shared CSS viewport and DPR, then derives four startup images per unique triple for light/dark portrait/landscape coverage. The current table has 22 triples and emits 88 startup images. Landscape links retain the portrait CSS viewport dimensions while their raster dimensions swap.

Family labels are checked against Apple’s iPhone Air, iPad, iPad mini, iPad Air, and iPad Pro display specifications. The iPhone 15 and 16 viewport mappings are checked against Chromium DevTools device presets, which specify 393 × 852 at 3× for iPhone 15, 15 Pro, and 16, and 430 × 932 at 3× for iPhone 15 Plus and 16 Plus.

Safe areas and toasts

App.Root owns the viewport-safe content boundary by default. It pads its outer canvas once, clips its content to that padded boundary, and marks descendants as having consumed the content safe area. Viewport-level portals retain the physical env(safe-area-inset-*) values.

Use var(--safe-area-inset-*) for in-flow content inside the padded root; use env(safe-area-inset-*) for anything positioned against the viewport or the root’s padding box.

@o/pipeline/theme.css provides physical and logical padding, margin, and inset safe-area utilities, including numeric spacing variants. See the theme utilities reference for the complete family and its physical/logical semantics.

Toaster owns viewport-safe defaults: 1.5rem on desktop and 1rem on mobile plus direct physical env() insets. Its mobile styling accounts for asymmetric left/right insets and Sonner’s RTL positioning. Callers can still pass explicit offset and mobileOffset values, while each app keeps its existing theme ownership.