Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Pwa Laravel Package

ladumor/laravel-pwa

Add Progressive Web App support to any Laravel app in minutes: generates the manifest and service worker, enables offline caching, push notifications, and an install prompt, and publishes configurable icons and meta tags via a simple artisan command, with no build tools required.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish

composer require ladumor/laravel-pwa
php artisan vendor:publish --provider="Ladumor\LaravelPwa\PWAServiceProvider"

Laravel auto-discovers the service provider. The publish step drops config/pwa.php (manifest metadata, icon map, offline toggle) and, depending on version, an offline view plus icon assets. From v1.0.1 there is also a command that publishes a debug asset on demand — handy when you want to inspect what the package thinks it's serving without reading vendor/. After publishing, run php artisan list | grep -i pwa — the package ships an artisan namespace (laravel-pwa:*) whose exact commands vary between releases, and that's your quickest inventory of what this version can generate for you.

2. Look at the config first

Everything you'll tune day-to-day lives in config/pwa.php:

return [
    'manifest' => [
        'name'       => env('APP_NAME', 'Laravel PWA'),
        'short_name' => env('APP_NAME', 'Laravel'),
        'start_url'  => '/',
        'display'    => 'standalone',
        'background_color' => '#ffffff',
        'theme_color'      => '#0d6efd',
        'icons' => [
            '192x192' => ['path' => '/icons/icon-192x192.png', 'type' => 'image/png'],
            '512x512' => ['path' => '/icons/icon-512x512.png', 'type' => 'image/png'],
        ],
    ],
    'enable'  => true,
    'offline' => true,
];

This array is the single source of truth: it's serialised by the package's route into /manifest.json and echoed into the <head> by the Blade directive.

3. Wire it into your root layout

Add the two directives to the single layout every page inherits (resources/views/layouts/app.blade.php):

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    @PwaHead   {{-- manifest link, theme-color, apple-touch-icon, meta tags --}}
</head>
<body>
    @yield('content')
    @PwaScript {{-- registers /serviceworker.js --}}
</body>
</html>

4. Verify before you build anything else

php artisan serve
curl -s http://localhost:8000/manifest.json | jq .
curl -sI http://localhost:8000/serviceworker.js

Then open Chrome DevTools → Application → Manifest (should render with zero warnings) and Service Workers (should show activated and is running). That 60-second check catches ~90% of PWA misconfiguration, and it's the first thing to do on any new project. Watch the console while you do it: v1.0.1 fixes a debug console error, so if you're coming from 1.0.0 and saw stray console.error noise on load, that's the release that clears it.

5. First real use case

The highest-value, lowest-effort win is a read-only shell: manifest + service worker + a precached offline fallback page. That alone gets you "Add to Home Screen" on Android, standalone launch, and a graceful offline experience — no custom caching logic required. Ship that first, measure installs, and only then start hand-tuning the service worker.

Implementation Patterns

Config-driven manifest, environment-aware. Keep brand values in config/pwa.php but source the volatile bits from .env so staging and production don't share a manifest:

'theme_color' => env('PWA_THEME_COLOR', '#0d6efd'),
'start_url'   => env('PWA_START_URL', '/'),
'name'        => env('PWA_NAME', config('app.name')),

Note start_url should match your authenticated landing page (/dashboard), not / — a start_url that 302s to login makes the installed app feel broken on every cold launch.

Serve the manifest dynamically instead of committing a static file. The package's route approach wins over public/manifest.json because the payload follows config, and config follows env. For multi-tenant or white-label setups, override the route and build the array from the current tenant:

// routes/web.php — after the package's routes are registered
Route::get('/manifest.json', function () {
    $tenant = app('tenant');

    return response()->json([
        'name'       => $tenant->name,
        'start_url'  => '/?tenant=' . $tenant->slug,
        'theme_color'=> $tenant->brand_color,
        // ...
    ], 200, ['Content-Type' => 'application/manifest+json']);
});

Use the debug asset as a local diagnostics view (v1.0.1+). The new publish-debug command gives you a package-shipped debug asset you can drop into a dev environment and hit in the browser to confirm the manifest/service worker wiring — much faster than curl-and-squint when you're chasing a registration failure. Publish it in local/staging only; see the gotcha below about keeping it out of production.

Deploy-pipeline hook. The manifest and service worker must be regenerated whenever config changes, and config:cache silently freezes old values:

php artisan config:cache
php artisan route:cache
# if your version exposes a generator command, run it after config:cache

Pin the patch line. ladumor/laravel-pwa:^1.0.1 is safe to adopt — 1.0.1 is a patch release (a debug-asset publish command plus a console-error fix), with no config keys or routes changed. Because the package moves slowly, pin a caret constraint and read the release notes on each bump rather than floating *.

Cache versioning on every deploy. The single most common post-launch bug is users stuck on yesterday's JS. Whatever cache name the shipped service worker uses, treat it as a build stamp — derive it from a commit hash or APP_VERSION env var and bump it on each release. If the package doesn't expose that hook, copy serviceworker.js into your own controller/view and own the CACHE_NAME constant.

Cache strategy that matches a Laravel app. Default package behaviour caches the shell; for a real app you want network-first for HTML and cache-first for hashed assets:

  • Network-first: navigation requests, /manifest.json, anything rendered by Blade.
  • Cache-first: /build/* (Vite output is content-hashed, so it's immutable), fonts, images.
  • Network-only: /api/*, /login, /logout, anything with CSRF in the path.

Never cache.put() a response to an authenticated route — one user's dashboard cached on a shared device is both a bug and a disclosure.

Install-prompt UX. There's no package API for this; handle it yourself in a small Alpine/Livewire component:

let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    // reveal your "Install app" button
});

Dismiss permanently once declined (localStorage), and only show it after the user has had one meaningful interaction — showing it on first paint trains people to ignore it. Note beforeinstallprompt never fires on iOS; you'll need a manual "Share → Add to Home Screen" hint there.

Testing. Add two cheap guards to your CI:

public function test_manifest_is_valid(): void
{
    $this->get('/manifest.json')
        ->assertOk()
        ->assertHeader('Content-Type', 'application/manifest+json')
        ->assertJsonPath('start_url', '/dashboard')
        ->assertJsonCount(2, 'icons');
}

Plus a Lighthouse CI budget (performance >= 0.9, pwa category checks) run against a built preview, not artisan serve.

Gotchas and Tips

Service workers only run over HTTPS or localhost. On a plain http://192.168.x.x dev box, registration silently fails with a console error most people filter out. Use valet secure, herd, or an ngrok tunnel for device testing — this is the #1 "the package doesn't work" report.

Cache the config, then forget you did. php artisan config:cache is standard in deploy scripts, and afterwards edits to config/pwa.php and .env do nothing until you re-cache. If the manifest looks stale on staging, run php artisan config:clear before debugging anything else.

The manifest file itself gets cached by the browser. Chrome caches /manifest.json aggressively. During development, DevTools → Application → Update on reload, and don't serve it with a long Cache-Control. In production give it a short TTL — a stale manifest means your icon and name changes never reach installed users.

Icons are where installability dies. Chrome requires a real 192×192 and 512×512 PNG; SVG is not accepted for installability, and a mislabelled .png that's actually a JPEG fails silently. Add "purpose": "maskable" to the 512 icon or Android will crop your logo into a circle and clip the edges. Verify with DevTools → Manifest → "Installability" — it names the exact missing piece.

Scope mismatches. The service worker's scope is determined by its URL. Served from /serviceworker.js, it controls the whole origin — good. If you ever move it to /js/sw.js (or let a CDN rewrite the path), it only controls /js/* and precaching breaks in ways that look like random network errors. Also ensure start_url falls inside that scope.

A stuck service worker will haunt you. If pages stop updating, unregister manually: DevTools → Application → Service Workers → Unregister, then Application → Storage → Clear site data. Hard-reload alone does not evict an active worker. Build a habit of doing this after every deploy-pipeline change.

skipWaiting is a trade-off, not a free win. Calling self.skipWaiting() activates the new worker immediately, but if it takes over mid-session with a different asset manifest, users can hit chunk-load errors on your Vite bundles. Safer default: skipWaiting() plus clients.claim(), and keep chunk hashes content-addressed so old chunks aren't orphaned.

Offline page must be fully self-contained. The fallback HTML is served when there's no network — no CDN fonts, no external CSS, no @vite. Inline the styles. Test it properly with DevTools → Network → Offline, and also test the cold-start case (offline and fresh cache) rather than only the warm one.

Don't let the worker intercept what Laravel owns. POST requests, /logout, file uploads, and anything with an X-CSRF-TOKEN should pass through untouched. A worker that responds to a POST from cache produces the most confusing 419s you'll ever debug.

Keep the debug asset out of production (v1.0.1+). The published debug asset is exactly that — a debug aid. Gate it behind APP_DEBUG or exclude it from your deploy artifact; an endpoint that echoes manifest config and service-worker state is a small but real information disclosure on a live host. If you publish it with the vendor:publish command, add it to your .gitignore/deploy excludes rather than committing it.

Extension points, in order of preference. (1) Config values — always try this first. (2) view()->composer / publishing and overriding the offline view. (3) Override the package routes with your own controller returning custom JSON — clean and upgrade-safe. (4) Publish and edit the service worker source directly — last resort, because you now own merges. The package is thin enough that options 2–3 cover essentially everything.

Know what you're depending on. MIT-licensed and small enough to read in one sitting, but it has effectively no downstream dependents and a very low release cadence. That's not a blocker for a package this size — the directives and the two routes are trivially reproducible — but it does mean you should skim vendor/ladumor/laravel-pwa/src once rather than assume the README matches the release you installed. v1.0.1 is a good example: the changelog is two lines and the useful detail (the debug-publish command's name) is only discoverable via php artisan list. If you find yourself fighting it, writing your own manifest route and a ~40-line service worker is a genuinely reasonable exit; you lose nothing but the Blade directives.

Debugging checklist, in order: DevTools → Application (registration + manifest) → Console (registration errors — and on 1.0.0, package-emitted console.error noise, fixed in 1.0.1) → chrome://serviceworker-internals (bypass DevTools quirks) → Network tab with "Disable cache" unchecked (to see what the worker actually serves). If the worker registered but nothing is cached, check the install event's cache.addAll() — one 404 rejects the whole batch and install fails wholesale, which is why a single broken icon path can take down the entire PWA.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin