Laravel

Keys in config, one Blade partial in your layout. Using Inertia with React or Vue? Follow that framework instead.

Installed with script tags in your templates · Written for Laravel 11 and 12, Blade

What works

ProductStatusNote
Analytics & custom eventsSupported
Feedback widgetSupported
Review badgeSupported
Web pushSupportedpublic/ is served at the site root
Roadmap embedSupported
Feedback, survey & roadmap linksSupported

1. Add your keys

Only the keys for products you use - the host is always needed.

.env

USERLILY_HOST=https://dev.userlily.com
USERLILY_TRACKING_ID=YOUR_TRACKING_ID
USERLILY_CAMPAIGN=YOUR_CAMPAIGN_KEY
USERLILY_BROADCAST_KEY=YOUR_PROJECT_KEY

2. Read them in config

Through config/services.php, not env() in a template: once config is cached, env() returns null outside config files.

config/services.php - add to the returned array

'userlily' => [
    'host' => env('USERLILY_HOST'),
    'tracking_id' => env('USERLILY_TRACKING_ID'),
    'campaign' => env('USERLILY_CAMPAIGN'),
    'broadcast_key' => env('USERLILY_BROADCAST_KEY'),
],

3. Create the partial

Each product loads only when its key is set.

resources/views/partials/userlily.blade.php

@php($ul = config('services.userlily'))

@if ($ul['tracking_id'])
<script src="{{ $ul['host'] }}/v1/tracker.js"
  data-tracking-id="{{ $ul['tracking_id'] }}" async></script>
@endif
@if ($ul['campaign'])
<script src="{{ $ul['host'] }}/v1/widget.js"
  data-campaign-id="{{ $ul['campaign'] }}" async></script>
@endif
@if ($ul['broadcast_key'])
<script src="{{ $ul['host'] }}/v1/broadcast.js"
  data-project="{{ $ul['broadcast_key'] }}" defer></script>
@endif

4. Include it in your layout

Just before </body>. Component layouts ({{ $slot }}) take the same include.

resources/views/layouts/app.blade.php

<body>
    @yield('content')

    @include('partials.userlily')
</body>

5. Send custom events

From any page. window.userlily exists once the tracker has loaded - well before anyone can submit a form.

In a template

<script>
  document
    .querySelector('#signup-form')
    .addEventListener('submit', function () {
      window.userlily.track('signup_completed')
    })
</script>

6. Show the review badge

The badge renders exactly where the tag sits. Switch on “Enable syndication” on the campaign’s Syndication page first.

In a template

{{-- Where the badge should appear --}}
<script
  src="{{ config('services.userlily.host') }}/v1/syndication.js"
  data-token="YOUR_CAMPAIGN_KEY" async></script>

7. Host the push worker

The worker must be served from your own domain at /userlily-sw.js, as JavaScript, with no redirect and over HTTPS. Each of these fails silently. Laravel serves public/ at the site root, so download it there.

Terminal, in your project root

curl -o public/userlily-sw.js https://dev.userlily.com/v1/userlily-sw.js

8. Ask for push permission from your own button

Optional. Works when the project’s “When to ask” is set to “Only when your own code asks”.

In a template

<button id="notify-me">Get notified</button>
<script>
  var button = document.querySelector('#notify-me')
  button.addEventListener('click', function () {
    // Exists once broadcast.js has started, and only
    // when "When to ask" is "Only when your own code asks".
    if (window.userlilyBroadcast) window.userlilyBroadcast.prompt()
  })
</script>

9. Embed the public roadmap

Turn on “Enable public roadmap” in Roadmap → Settings first. An Allowed domain on the roadmap limits which sites can embed it.

Where the roadmap should appear

<iframe
  data-userlily-roadmap
  src="https://dev.userlily.com/r/YOUR_ROADMAP_KEY?embed=1"
  title="Product roadmap"
  style="width:100%;height:720px;border:0"
  loading="lazy"
></iframe>
<script>
(function () {
  window.addEventListener("message", function (event) {
    if (event.origin !== "https://dev.userlily.com") return;
    var data = event.data;
    if (!data || data.type !== "userlily:roadmap:height") return;
    var frame = document.querySelector("iframe[data-userlily-roadmap]");
    if (frame && data.height > 0) frame.style.height = data.height + "px";
  });
}());
</script>

Notes

  • Inertia with React or Vue: follow the React or Vue guide instead. Laravel’s Vite exposes VITE_-prefixed variables from .env to the browser.

Every option, limit and troubleshooting step is in the main docs. Logos are trademarks of their owners and are shown only to indicate compatibility.