A utility for generating polished, print-ready PDF installation guides for Progressive Web Apps (PWAs). It renders step-by-step iOS Safari and Android Chrome installation flows inside realistic phone frames, with full internationalisation support, then exports them as high-resolution PNGs and combines them into a PDF.
Each locale produces a 3-page PDF (exports/<locale>/<locale>-pwa-guide.pdf):
| Page | Content |
|---|---|
| 1 | Cover — app icon, title, locale badge |
| 2 | iOS · Safari — steps 1–4 inside an iPhone frame |
| 3 | Android · Chrome — steps 1–4 inside an Android frame |
PNGs are also saved individually in exports/<locale>/ for use in other documents or presentations.
- Node.js 18+
- npm 9+
npm install
npx playwright install chromium# English only
npm run capture
# All languages (en, no, pl)
npm run capture:allOutput lands in exports/:
exports/
├── en/
│ ├── cover.png
│ ├── ios.png
│ ├── android.png
│ └── en-pwa-guide.pdf
├── no/
│ └── no-pwa-guide.pdf
└── pl/
└── pl-pwa-guide.pdf
npm run dev
# Open http://localhost:5173/enSupported routes: /en, /no, /pl
React SPA (Vite)
│
├── /:locale ← dev/preview UI (both platforms side-by-side)
└── /export/:locale/
├── cover ← cover page
├── ios ← iOS steps only, scale 0.27
└── android ← Android steps only, scale 0.26
Playwright (headless Chromium)
└── Screenshots each export route at 1400×750 × 2× DPR → 2800×1500 PNG
pdf-lib
└── Embeds the 3 PNGs into a single PDF
Each phone is rendered at native resolution inside a CSS transform: scale() wrapper:
- iOS frame (
frame-dark.svg) — 1079×2178 px native, scaled to ~291×588 px on the export page - Android frame (
frame-light.svg) — 1148×2318 px native, scaled to ~298×603 px on the export page
The screenshot image is clipped to the transparent screen opening in the SVG frame using overflow: hidden with precise pixel insets derived from the frame geometry.
Each step renders a different overlay on top of the screenshot:
| Step | iOS | Android |
|---|---|---|
| 1 | Safari address bar (floating pills, red ring on ⋯) | Chrome address bar (red ring on ⋮) |
| 2 | Safari share popover | Chrome three-dots dropdown (Add to home screen highlighted) |
| 3 | iOS Share Sheet (Add to Home Screen highlighted) | "Add to home screen" bottom sheet (Install highlighted) |
| 4 | "Add to Home Screen" dialog + keyboard | PWA install prompt (Install button highlighted) |
iOS overlays use the iOS 26 liquid glass aesthetic — frosted white pills and sheets with backdrop-filter: blur(). Android overlays use plain white cards matching Material You Chrome UI.
Glass style tokens are centralised in src/lib/glass.ts and shared across all components.
Translations live in src/i18n/:
src/i18n/
├── en.json ← English
├── no.json ← Norwegian (Bokmål)
├── pl.json ← Polish
└── index.ts ← LocaleContext, useT() hook, getTranslations()
Every UI string in the overlay components is pulled from the active translation via useT(). The app name and URL (My App, example.com) are intentionally not translated — replace these with your own brand strings.
The locale is set from the URL path (/:locale or /export/:locale/...) and injected via React Context, so no prop drilling is needed.
Background screenshots are locale-specific (different language in the app UI):
| Locale | iOS screenshot | Android screenshot |
|---|---|---|
en |
screen-ios.jpg |
screen-en.png |
no |
screen-ios.jpg |
screen-no.png |
pl |
screen-ios.jpg |
screen-pl.png |
Screenshot paths are defined in src/lib/screens.ts.
Drop your own screenshots into public/:
public/
├── screen-en.png ← Android, English UI
├── screen-no.png ← Android, Norwegian UI
├── screen-pl.png ← Android, Polish UI
└── screen-ios.jpg ← iOS (used for all locales)
Update src/lib/screens.ts if you use different filenames.
Set VITE_APP_ICON in .env.local to point to your icon (local path under public/local/ or a remote URL).
Update the hardcoded brand strings "My App" and "example.com" in ShareActionSheet.tsx, AddToHomeScreen.tsx, AndroidAddToHome.tsx, AndroidInstallPrompt.tsx, SafariBar.tsx, ChromeBar.tsx, and ExportCover.tsx.
- Add a new JSON file in
src/i18n/(e.g.de.json) usingen.jsonas a template. - Register it in
src/i18n/index.ts:import de from "./de.json"; export const LOCALES = ["en", "no", "pl", "de"] as const; const translations = { en, no, pl, de };
- Add an Android screenshot for the new locale and update
src/lib/screens.ts. - Run
npm run capture:all— it will pick up the new locale automatically.
In scripts/capture.ts:
const SCALE = 2; // increase to 3 for 4200×2250 outputHigher values produce sharper PNGs at the cost of larger file sizes.
├── public/
│ ├── frame-dark.svg iPhone frame (iOS)
│ ├── frame-light.svg Android frame
│ ├── icon-placeholder.png Placeholder app icon (default)
│ ├── screen-placeholder.png Placeholder screenshot (default)
│ └── local/ Your real assets (git-ignored)
│
├── src/
│ ├── i18n/ Translation files + context
│ ├── lib/
│ │ ├── glass.ts iOS 26 glassmorphism style tokens
│ │ └── screens.ts Locale → screenshot mapping
│ ├── components/
│ │ ├── PhoneFrame.tsx iOS frame wrapper
│ │ ├── PhoneFrameAndroid.tsx Android frame wrapper
│ │ ├── SafariBar.tsx iOS 26 floating address bar
│ │ ├── ShareSheet.tsx iOS ⋯ popover menu
│ │ ├── ShareActionSheet.tsx iOS full share sheet
│ │ ├── AddToHomeScreen.tsx iOS "Add to Home Screen" dialog
│ │ └── android/
│ │ ├── ChromeBar.tsx Chrome address bar + nav
│ │ ├── ChromeMenu.tsx Chrome three-dots dropdown
│ │ ├── AndroidAddToHome.tsx "Add to home screen" sheet
│ │ └── AndroidInstallPrompt.tsx PWA install prompt
│ ├── pages/
│ │ ├── ExportCover.tsx Cover page (export route)
│ │ ├── ExportIOS.tsx iOS steps page (export route)
│ │ └── ExportAndroid.tsx Android steps page (export route)
│ ├── App.tsx Main dev/preview layout
│ └── main.tsx Router setup
│
├── scripts/
│ └── capture.ts Playwright + pdf-lib export script
│
└── exports/ Generated output (git-ignored)
├── en/
├── no/
└── pl/
| Tool | Purpose |
|---|---|
| Vite + React + TypeScript | App framework |
| Tailwind CSS v4 | Utility classes for the dev preview shell |
| React Router v7 | Locale-based routing (/en, /no, /pl) |
| Playwright | Headless Chromium screenshot capture |
| pdf-lib | PNG → PDF assembly |
| tsx | Zero-config TypeScript script runner |


