foxws/laravel-pwa
Laravel PWA adds progressive web app support to Laravel apps with manifest and service worker generation, install prompts, icons, and offline caching so users can install your site and use it like a native app.
foxws/laravel-pwa is a generation-based package, not a runtime one: you configure PHP, run an Artisan command that emits public/manifest.json and public/sw.js, and inject two Blade directives into your root layout. Almost every "it doesn't work" moment traces back to skipping the generate step after a config change.
Minimal path:
composer require foxws/laravel-pwa
php artisan vendor:publish --tag="pwa-config"
php artisan storage:link # default icons expect storage URLs
php artisan pwa:generate # writes public/manifest.json + public/sw.js
Then in your root layout (one layout only):
<head>
@pwaHead
</head>
<body>
...
@pwaSw
</body>
Sanity check that the artifacts exist and are versioned:
ls -l public/manifest.json public/sw.js
# both URLs get a ?v= suffix derived from file mtime
Where to look first:
config/pwa.php — the manifest array, icons list, ignore_paths, update_interval, and the enabled flag. This is the source of truth; the files in public/ are generated output.docs/usage.md and docs/configuration.md — short, and more accurate than reading the package source.public/sw.js — it's a readable stub, and it's what actually governs caching. Read it once so you know what the defaults do before you customize anything.First real use case: put @pwaHead/@pwaSw in the layout, add 192×192 and 512×512 PNGs plus an apple-touch-icon where config('pwa.icons') expects them, run pwa:generate, and stop. Verify in Chrome DevTools → Application → Manifest that installability is green and the icons render. Don't touch cache strategy or ignore_paths until the install flow works on a real device — that ordering saves you from debugging two layers at once.
Configure through env, generate on deploy. The manifest fields are .env-driven (PWA_SHORT_NAME, PWA_DESCRIPTION, PWA_THEME_COLOR, PWA_BACKGROUND_COLOR, PWA_DISPLAY, PWA_START_URL), with name coming from APP_NAME. So the natural workflow is: per-environment .env, then php artisan pwa:generate as a deploy step, not a commit-the-artifacts step. That keeps staging and production manifests honest.
Pass through arbitrary manifest keys. Any key added to the manifest array is emitted verbatim, and a key set to null is omitted. That's the extension point for shortcuts, screenshots, categories, id, orientation, display_override:
'manifest' => [
'shortcuts' => [
['name' => 'Dashboard', 'url' => '/dashboard'],
],
'screenshots' => null, // omit entirely
],
Icons: disk vs. literal path. Each icon entry is disk, path, sizes, type. With disk set, the URL is resolved via Storage::disk()->url() at generation time — meaning the resolved absolute URL is baked into manifest.json. With disk => null, path is used as-is, which is what you want if you serve icons from public/ or a CDN with a relative URL. Decide this early; switching modes later means regenerating and re-verifying on device.
One layout, one registration. Duplicating @pwaSw across multiple layouts is how you get two service workers contending for scope. Use it once in the app shell. Override per-need with the directive arguments rather than editing the published template:
@ pwaSw(['swPath' => '/sw.js', 'scope' => '/', 'debug' => true, 'updateInterval' => 12])
@ pwaHead(['themeColor' => '#ff0000', 'manifest' => '/custom.json', 'icon' => '/icon.png'])
debug => true is the cheap switch to turn on when registration silently no-ops.
Keep ignore_paths current as you add XHR surfaces. The service worker already skips Inertia (X-Inertia), Livewire (X-Livewire, X-Livewire-Navigate) and htmx (HX-Request), plus anything in pwa.ignore_paths. Every new API prefix, autocomplete endpoint, polling route, or JSON fetch you add is a candidate. Make adding to ignore_paths part of the PR checklist when you introduce a new fetch() call, rather than discovering it as a stale-response bug weeks later.
Tune update_interval deliberately. It's in hours, default 24; 0 disables periodic update checks. Shorten it during an active release window if you need clients to pick up changes faster, then put it back.
Ship a kill switch and exercise it. PWA_ENABLED=false plus pwa:generate writes a self-unregistering service worker that clears caches and removes itself — the Blade templates and manifest.json are left unchanged. Put this in your runbook and test it once in staging, because a self-unregistering SW that fails on first use is not a rollback plan.
Own the offline page. public/offline.html is yours to create — the package doesn't ship it (there's an example in the repo). Treat it as a real page: brand it, explain what's unavailable, link back to a safe route.
If you use Laravel Boost (2.6.0). The release adds a Boost skill encoding the package's conventions — generate-don't-hand-edit, directive syntax, cache-busting. It's inert at runtime and purely additive; it just means an AI assistant asked to "make the dashboard offline-capable" produces @pwaHead/@pwaSw and a pwa:generate step instead of an ad-hoc Workbox setup.
@pwaHead and @pwaSw are Blade includes, not components. <x-pwa-head /> will not resolve. This is the single most common first-hour mistake.config/pwa.php or .env changes nothing until php artisan pwa:generate runs. Conversely, never hand-edit public/manifest.json or public/sw.js — they are overwritten output.config:cache and route:cache but not pwa:generate, production serves whatever was last generated. Order it after config changes, before the app goes live.?v= suffix comes from each file's last-modified time, so a pwa:generate run busts caches. But artifact copies that preserve mtimes (rsync -a, some tar/CI artifact flows) can defeat this if you build the files in one stage and copy them in another — generate in the final stage, or verify the suffix actually changed after a deploy.PWA_ENABLED=false is a soft disable. The service worker unregisters and clears caches, but the Blade templates and manifest.json are unchanged. Expect the manifest link to remain in the HTML; don't be surprised by it.storage:link and APP_URL. Default icons live under /storage/images/icons/, so storage:link is required, and with disk set the resolved URL is baked at generation time using the app URL. A wrong APP_URL behind a proxy produces a manifest served from one origin referencing icons on another, and installability fails with no visible error. Configure TrustProxies and regenerate.http://192.168.x.x. Testing from a phone against php artisan serve --host=0.0.0.0 will look broken for reasons that aren't the package's fault./ — a previous package, a CMS, an in-house Workbox build — will conflict. Audit the Application panel before you install.ignore_paths gaps surface late. The Inertia/Livewire/htmx headers are covered for free; your own fetch() calls and API prefixes are not. Symptom is a stale or blank response in a component that works on hard reload.@pwaHead handles the apple-touch-icon and theme-color, but there is no beforeinstallprompt on Safari — install UX is a manual "Add to Home Screen" instruction. Never gate onboarding on a prompt iOS won't fire.pwa:generate run and are the ?v= values fresh, (2) DevTools → Application → is the SW active and is the manifest error-free, (3) Network tab → is the request reaching the server at all, (4) Lighthouse installability. Jumping to cache-rule surgery skips steps that explain most failures.config/pwa.php and your own public/offline.html means removal is a one-PR operation, and every future bump is a changelog read rather than a code audit.How can I help you explore Laravel packages today?