slfomin / pwa-laravel
Comprehensive PWA integration for Laravel 13 + Vite 7 with Inertia v3 support
Fund package maintenance!
Requires
- php: ^8.4
- illuminate/contracts: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- intervention/image: ^3.10
- spatie/laravel-package-tools: ^1.93
Requires (Dev)
- inertiajs/inertia-laravel: ^3.0
- larastan/larastan: ^3.0
- laravel/pint: ^1.18
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- rector/rector: ^2.0
- spatie/laravel-ray: ^1.40
Suggests
- inertiajs/inertia-laravel: Required to use Inertia v3 adapter (^3.0)
README
Full PWA integration for Laravel 13 + Vite 7 with optional Inertia.js v3 support.
- Static or dynamic Web App Manifest (per-locale, per-tenant via resolvers)
- Service Worker registration via Blade directives
- Automatic icon generation (standard, maskable, apple-touch, favicon) via
intervention/image - Inertia v3 adapter: shared PWA props, middleware for correct SW/Inertia caching
- JS companion package
@slfomin/pwa-laravelwrappingvite-plugin-pwa - PHPStan level 8, PHP 8.4+
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.4 |
| Laravel | ^13.0 |
| Vite | ^6.0 || ^7.0 |
| vite-plugin-pwa | ^1.0 |
| Inertia.js (optional) | ^3.0 |
Installation
ddev composer require slfomin/pwa-laravel
Run the interactive installer:
ddev artisan pwa:install
This publishes config/pwa.php and prints the next steps.
Quick Start
1. Place your source icon
Put a square PNG (512×512 or larger) at resources/images/pwa-icon.png, then generate all sizes:
ddev artisan pwa:generate-icons
Icons are written to public/icons/ and cover standard, maskable, Apple Touch, and favicon sizes.
2. Add Blade directives to your layout
<!DOCTYPE html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> @pwaMeta @vite(['resources/css/app.css', 'resources/js/app.js']) </head> <body> {{ $slot }} @pwaRegisterSW </body> </html>
@pwaMeta renders the <link rel="manifest">, theme-color, apple-touch-icon, favicons, and other mobile meta tags.
@pwaRegisterSW injects a small inline script that registers the service worker on page load.
3. Install the JS companion
Via npm (recommended):
ddev npm install -D vite-plugin-pwa @slfomin/pwa-laravel
From the composer package (no npm required):
Pre-built files are shipped inside the composer package at vendor/slfomin/pwa-laravel/dist/.
Import directly in vite.config.js:
import { laravelPwa } from '../../vendor/slfomin/pwa-laravel/dist/index.js'; // framework composables (optional): // import { usePwa } from '../../vendor/slfomin/pwa-laravel/dist/vue.js';
vite-plugin-pwais still a required peer dependency — install it withddev npm install -D vite-plugin-pwa.
4. Configure Vite
// vite.config.js import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import { laravelPwa } from '@slfomin/pwa-laravel'; export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true, }), laravelPwa({ strategies: 'generateSW', manifest: false, // manifest is served by Laravel, not Vite workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg,webp,woff,woff2}'], }, }), ], });
5. Build
ddev npm run build
That's it — your Laravel app is now a PWA.
Inertia.js v3 Integration
Vite config
import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import inertia from '@inertiajs/vite'; import { laravelPwa } from '@slfomin/pwa-laravel'; export default defineConfig({ plugins: [ laravel({ input: ['resources/css/app.css', 'resources/js/app.js'], refresh: true }), inertia(), laravelPwa({ inertia: true, // sets navigateFallback: '/' and excludes API routes strategies: 'generateSW', manifest: false, devOptions: { enabled: false }, // must be off when inertia() plugin is active }), ], });
Shared props
The package automatically calls Inertia::share('pwa', ...) if Inertia is installed and
pwa.inertia.auto_detect is true (default). Every page receives:
{ pwa: { manifest_url: string, sw: { url, scope, register_type, auto_register, available }, navigate_fallback: string | null, is_ssr: boolean, } }
usePwa() — Vue 3
import { usePwa } from '@slfomin/pwa-laravel/vue'; const { manifestUrl, swInfo, navigateFallback, isOffline } = usePwa();
usePwa() — React 19
import { usePwa } from '@slfomin/pwa-laravel/react'; const { manifestUrl, swInfo, navigateFallback, isOffline } = usePwa();
Middleware
Add pwa.inertia to your routes to set correct Vary and Cache-Control headers,
preventing the service worker from caching Inertia XHR responses:
// routes/web.php Route::middleware(['web', 'pwa.inertia'])->group(function () { // your Inertia routes });
Dynamic Manifest
Switch the driver to dynamic to serve a manifest generated by Laravel on each request:
// config/pwa.php 'manifest' => [ 'driver' => 'dynamic', 'dynamic' => [ 'resolver' => \App\Pwa\TenantManifestResolver::class, 'cache' => true, 'cache_ttl' => 3600, ], ],
Implement your resolver:
namespace App\Pwa; use Illuminate\Http\Request; use SlFomin\PwaLaravel\Contracts\ManifestResolver; use SlFomin\PwaLaravel\Manifest\ManifestBuilder; final class TenantManifestResolver implements ManifestResolver { public function resolve(Request $request, ManifestBuilder $default): ManifestBuilder { $tenant = $request->user()?->tenant; return $tenant ? $default->name($tenant->name)->themeColor($tenant->brand_color) : $default; } public function cacheKey(Request $request): ?string { return 'tenant.'.($request->user()?->tenant_id ?? 'guest'); } }
Bind it in AppServiceProvider:
$this->app->bind( \SlFomin\PwaLaravel\Contracts\ManifestResolver::class, \App\Pwa\TenantManifestResolver::class, );
Blade Directives
| Directive | Output |
|---|---|
@pwaMeta |
<link rel="manifest">, theme-color, apple-touch-icon, favicons, mobile meta |
@pwaRegisterSW |
Inline <script> registering the service worker |
@pwaInstallButton('Install') |
A hidden <button> that appears when the browser's install prompt fires |
Artisan Commands
| Command | Description |
|---|---|
pwa:install |
Interactive installer — publishes config and prints next steps |
pwa:generate-icons |
Generate full icon set from source PNG |
pwa:publish-manifest |
Write manifest.webmanifest from config (no Vite build required) |
pwa:generate-icons
pwa:generate-icons [source] [--output=] [--dry-run]
source Path to source PNG (≥512×512, square). Default: pwa.icons.source from config.
--output Output directory. Default: pwa.icons.output_path from config.
--dry-run Validate source without writing any files.
pwa:publish-manifest
pwa:publish-manifest [--path=] [--pretty]
--path Output file path. Default: pwa.manifest.static_path from config.
--pretty Pretty-print the JSON output.
Configuration Reference
All options live in config/pwa.php. Key .env variables:
| Variable | Default | Description |
|---|---|---|
PWA_MANIFEST_DRIVER |
static |
static (Vite file) or dynamic (Laravel controller) |
PWA_MANIFEST_ROUTE |
/manifest.webmanifest |
URL the browser fetches |
APP_NAME |
Laravel |
PWA full name |
PWA_SHORT_NAME |
APP_NAME |
Short name for the home screen icon |
PWA_DESCRIPTION |
`` | PWA description |
PWA_DISPLAY |
standalone |
Display mode (standalone, fullscreen, minimal-ui, browser) |
PWA_THEME_COLOR |
#000000 |
Theme / status-bar color |
PWA_BG_COLOR |
#ffffff |
Splash screen background color |
PWA_SW_STRATEGY |
generateSW |
generateSW or injectManifest |
PWA_SW_URL |
/sw.js |
Service worker registration URL |
PWA_SW_DEV |
false |
Enable service worker in local dev |
PWA_MANIFEST_CACHE |
true |
Cache dynamic manifest responses |
PWA_MANIFEST_CACHE_TTL |
3600 |
Cache TTL in seconds |
Facade & Contracts
use SlFomin\PwaLaravel\Facades\Pwa; Pwa::manifest(); // ManifestBuilder for the current request Pwa::manifestUrl(); // string — URL of the manifest Pwa::serviceWorkerUrl(); // string — URL of the service worker Pwa::worker(); // WorkerManager instance Pwa::driver(); // ManifestDriver instance
Extension points in SlFomin\PwaLaravel\Contracts:
| Contract | Purpose |
|---|---|
ManifestDriver |
Custom manifest delivery strategy |
ManifestResolver |
Context-aware manifests (tenant, locale, user role) |
IconGenerator |
Replace intervention/image with another library |
ServiceWorkerStrategy |
Extend Vite plugin options |
Events
Hook into the PWA lifecycle with five Laravel events
(ManifestResolving, ManifestResolved, ServiceWorkerRequested, IconsGenerated,
ManifestPublished):
use SlFomin\PwaLaravel\Events\PwaEvents; use SlFomin\PwaLaravel\Events\ManifestResolved; PwaEvents::manifestResolved(function (ManifestResolved $event): void { // last-chance mutation before the JSON is serialized $event->manifest->name('My App — '.app()->getLocale()); });
See docs/events.md for the full reference.
Testing
ddev composer test # Pest (119 tests) ddev composer analyse # PHPStan level 8 ddev composer format # Laravel Pint ddev composer ci # all three in sequence
Changelog
Please see CHANGELOG for recent changes.
Contributing
Please see CONTRIBUTING for details.
Security
Please review our security policy to report vulnerabilities.
Credits
License
MIT. Please see License File for more information.