nowo-tech/pwa-bundle
A Symfony bundle for adding Progressive Web App capabilities to your site, with configuration for web app manifests, service workers, caching, and installability so users get a fast, app-like experience.
First, the reality check. nowo-tech/pwa-bundle is a Symfony bundle, not a Laravel package — and v1.11.0 does nothing to change that. The new option lands in the bundle's own configuration tree and is consumed by a Twig template, so there is still no PwaServiceProvider, no vendor:publish, no Blade directives, and Composer's Laravel auto-discovery will not register anything useful. If you composer require nowo-tech/pwa-bundle in a Laravel app you get a package whose DependencyInjection/Extension, Configuration tree, and Twig helpers are never loaded. Decide up front which path you're on:
Where to look first in the repo, in this order:
README.md — install command, bundle registration line (config/bundles.php), and the sample config/packages/pwa.yaml.DependencyInjection/Configuration.php — the authoritative list of options. This file is the documentation; the README is usually behind it. As of v1.11.0 expect an install_prompt node with at least mark_asset and mark_template.Resources/views/ — the Twig partial that emits <link rel="manifest">, theme-color, and the SW registration script, plus pwa/install_prompt.html.twig. Translate both to Blade. The install-prompt template is now the interesting file: it's the piece v1.11.0 exists to make overridable.Minimal Symfony path:
composer require nowo-tech/pwa-bundle:^1.11
// config/bundles.php
Nowo\PwaBundle\PwaBundle::class => ['all' => true], // confirm exact FQCN in composer.json
# config/packages/pwa.yaml
nowo_pwa:
install_prompt:
enabled: true
# mark_asset: 'images/logo.svg' # static asset
mark_template: 'pwa/mark.html.twig' # v1.11.0 — wins over mark_asset
manifest:
name: 'My App'
short_name: 'MyApp'
start_url: '/'
display: standalone
theme_color: '#0b5fff'
background_color: '#ffffff'
icons: []
service_worker:
enabled: true
Then bin/console cache:clear and bin/console debug:config nowo_pwa to confirm what actually got parsed — the config root name is the one thing the README and the Configuration.php tree most often disagree on, so trust debug:config over both.
First use case: ship an installable PWA on an existing app with zero asset-build changes — a manifest endpoint, a service worker endpoint, one <link> tag in your layout, and (v1.11.0) your own branded install prompt mark. Verify with DevTools → Application → Manifest (no errors, icons resolve) and Application → Service Workers (status activated), then run Lighthouse's PWA/Installability audit.
Laravel equivalent of "first use case": two routes plus one component.
// routes/web.php
Route::get('/manifest.webmanifest', [PwaController::class, 'manifest'])->name('pwa.manifest');
Route::get('/sw.js', [PwaController::class, 'serviceWorker'])->name('pwa.sw');
Serve the manifest with Content-Type: application/manifest+json and the worker with application/javascript, both from the site root. Add the <link rel="manifest" href="{{ route('pwa.manifest') }}"> to resources/views/layouts/app.blade.php.
Config-as-source-of-truth, endpoints-as-output. The bundle's core idea is that the manifest and service worker are routes, not static files in public/. That's the pattern worth stealing even if you never install it: generated-at-request (or generated-at-deploy) assets give you per-environment values, cache busting, and multi-tenant support for free.
Environment-driven config. Keep name, start_url, icons, and cache version in config, overridden per environment. In Laravel terms:
// config/pwa.php
return [
'name' => env('PWA_NAME', config('app.name')),
'short_name' => env('PWA_SHORT_NAME', 'App'),
'theme_color' => env('PWA_THEME_COLOR', '#0b5fff'),
'cache_version' => env('PWA_CACHE_VERSION', 'v1'),
'offline_url' => '/offline',
'install_prompt' => [
'enabled' => env('PWA_INSTALL_PROMPT', true),
'mark_view' => 'partials.pwa-mark', // your Blade analogue of mark_template
],
];
Layout integration. One partial, included once, in your base layout:
{{-- resources/views/partials/pwa.blade.php --}}
<link rel="manifest" href="{{ route('pwa.manifest') }}">
<meta name="theme-color" content="{{ config('pwa.theme_color') }}">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => navigator.serviceWorker.register('/sw.js'));
}
</script>
The v1.11.0 pattern worth copying: themable brand marks. The release exists because hosts were forking pwa/install_prompt.html.twig just to swap a logo. The fix — a mark_template (inline Twig/SVG) that takes precedence over a flat mark_asset path — is a good design and ports cleanly:
{{-- resources/views/partials/pwa-mark.blade.php --}}
<svg viewBox="0 0 32 32" role="img" aria-hidden="true" fill="currentColor">
<path d="..."/>
</svg>
Include it inside your install-prompt component and drive its colors from CSS custom properties, so dark mode and per-tenant theming need no config change. Keep the "template wins over asset" precedence — it's the right default (specific beats generic) and it means existing mark_asset configs keep working untouched.
Install prompt pattern. Capture beforeinstallprompt, stash the event, show your own button, call prompt() on click, and hide the button on appinstalled. Always feature-detect — Safari/Firefox won't fire it, and iOS requires the "Add to Home Screen" manual flow.
let deferred;
addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferred = e; showInstallButton(); });
installBtn.addEventListener('click', async () => { deferred?.prompt(); await deferred?.userChoice; deferred = null; hideInstallButton(); });
addEventListener('appinstalled', () => hideInstallButton());
Caching strategies, matched to content:
/offline.Multi-tenant / multi-locale manifests. Parameterize the manifest route (/manifest/{locale}.webmanifest) and set dir/lang. Keep the service worker single and global — one scope per origin — and let the worker handle locale routing internally. Registering multiple workers at different paths is a path to confusion.
Versioning discipline. Bump a cache name derived from config or a build hash on every deploy, and caches.delete() the old ones in activate. This is the single highest-value thing a PWA bundle can do for you; check whether this one does it, and if not, add it in your port.
Testing in CI. Run Lighthouse CI against the manifest and installability audits on PRs; it catches missing icons, wrong content types, and start_url unreachability before users do.
The package itself — updated read. The earlier "implausible release date / dead repo" worry is now partly answered: the project is shipping steady point releases (v1.10.2 → v1.11.0) with a release-check hash in the notes, which looks like an automated release pipeline. That's a maintenance-positive signal. Adoption signals are still thin, though, so keep treating it as small-audience production code: pin an exact version (^1.11), read the service worker source line by line before it touches production, and be ready to fork. There is still no community to ask when something breaks, and there is still no sibling Laravel package that would make installing this in a Laravel app sensible.
mark_template vs mark_asset precedence. The release notes say the template wins over the asset. That means a stale mark_template key left in a config file will silently shadow a mark_asset you just changed, and you'll "fix" the asset in vain. If your mark stops updating, debug:config nowo_pwa first — it will show both keys and you can see which one is live. Also confirm the template path resolution: Twig template names here are bundle-namespaced, so a bare pwa/mark.html.twig may resolve to the bundle's own copy rather than your templates/ override. If your new mark doesn't appear, you're likely editing the wrong file, not hitting a cache.
Custom mark templates need the same escaping care as any Twig |raw. Inline SVG is the stated use case, so whatever renders it is almost certainly un-escaped. If your mark ever interpolates tenant- or user-supplied strings (a company name, a logo URL), that's an XSS vector. Keep the SVG static, or escape every interpolation yourself. Same rule applies to your Blade port.
Service worker scope is the classic footgun. A worker served from /build/sw.js only controls /build/. Serve it from the origin root (/sw.js) with Service-Worker-Allowed: / if it must live elsewhere. If your install prompt never appears, check scope before anything else — and remember the prompt also requires the manifest to be valid and the page to be in a secure context.
Content types. application/manifest+json (or application/json) and application/javascript — not text/plain. Laravel's response()->json() is fine for the manifest; make sure the SW response isn't sent as JSON.
HTTPS or localhost only. Workers silently fail to register on plain HTTP on any other host. php artisan serve on localhost works; a LAN IP for mobile testing does not — use a tunnel.
Symfony config cache. After editing pwa.yaml — including adding install_prompt.mark_template — you must bin/console cache:clear (or run debug:config nowo_pwa first to prove it parsed). Forgetting this is the #1 source of "my new option does nothing" reports, especially on a config key that shipped this release.
Manifest caching by the browser. Browsers cache manifests aggressively and inconsistently. If icons or start_url don't update, append a hash or set Cache-Control: no-cache on the manifest route.
Don't generate the worker per-request from a DB. It fragments caches and slows first paint. Build it once at deploy, or render from static config only.
Icons. Ship 192×192 and 512×512 PNG, plus a purpose: "maskable" variant with safe-zone padding. One missing size fails installability and many Android launchers silently ignore a bad set. Your custom mark_template is branding, not an icon — don't try to make one SVG do both jobs.
Extension points if you stay on Symfony: override Resources/views/ from templates/bundles/<BundleName>/ for markup changes (v1.11.0 reduces how often you need to — prefer mark_template over a full prompt override), decorate the manifest generator service for data changes, and prefer a compiler pass or a config key over forking. In a Laravel port, mirror this with a PwaManifest class bound in a service provider plus a single Blade partial for the prompt — one class and one view, one place to override.
Debugging loop: DevTools → Application. Turn on "Update on reload" and "Bypass for network" while iterating, then turn both off to test real offline behavior. chrome://serviceworker-internals for registration state, Cache Storage panel for what's actually cached, and "Unregister" as your reset button. When a stale worker serves old HTML, hard-reload and unregister — reloading alone won't do it.
Finally: a PWA is a UX contract, not a checkbox. An installable app that serves a cached stale shell is worse than a plain website. Budget time for the offline story — what's cached, what's excluded, and what the user sees when the network is gone.
How can I help you explore Laravel packages today?