Laravel & PHP
Install StepsKit (Laravel & PHP)
For Laravel with Blade, and for plain PHP apps. Both are multi-page apps, which changes one important thing about identifying users — see step 2.
Using Laravel with Inertia? It behaves like a single-page app; read that section.
Prerequisites
- Your project API key, from the Integrations page in the dashboard.
- A layout template included by every page you want tours on.
1. Add the loader
Paste this into your root Blade layout — usually
resources/views/layouts/app.blade.php — just before </body>:
<script>
(function () {
var stepskit = window.stepskit = window.stepskit || {};
stepskit._q = stepskit._q || [];
// Queue API calls made before the SDK finishes loading.
var methods = ['identify', 'setUserAttributes', 'track', 'refresh', 'playTour', 'stopTour', 'dismissAnnouncement', 'on', 'off', 'destroy', 'validateEnvironment'];
methods.forEach(function (method) {
stepskit[method] = stepskit[method] || function () {
stepskit._q.push([method].concat([].slice.call(arguments)));
};
});
// Load the StepsKit SDK asynchronously.
var script = document.createElement('script');
script.async = true;
script.src = 'https://cdn.stepskit.com/stepskit.latest.js';
script.setAttribute('data-api-key', 'YOUR_API_KEY');
var first = document.getElementsByTagName('script')[0];
first.parentNode.insertBefore(script, first);
})();
</script>If your layout defines a script stack, push it there instead so it lands after your other scripts:
{{-- resources/views/layouts/app.blade.php --}}
@stack('scripts')
</body>
</html>{{-- any view --}}
@push('scripts')
{{-- the StepsKit loader --}}
@endpushPlain PHP: put the same snippet in whatever shared footer include every
page pulls in — footer.php, layout.php, or equivalent. There is nothing
Laravel-specific about the snippet itself.
2. Identify the user
This is half the install, not a nice-to-have. Without an id, show-once
capping degrades to a per-tab check and any tour with a "filtered" audience is
hidden outright.
In a multi-page app, user attributes do not survive a page load. Nothing
persists them, so identifying the user on the login page alone does nothing for
every page after it. The identify call has to live in the same global layout
as the loader, so it runs on every request.
Put it directly after the loader:
{{-- resources/views/layouts/app.blade.php --}}
@auth
<script>
stepskit.identify(@json([
'id' => (string) auth()->id(),
'email' => auth()->user()->email,
'plan' => auth()->user()->plan,
]));
</script>
@endauthEscape it properly
@json() is doing security work here, not formatting work. It encodes the array
as JSON and escapes it for a JavaScript context.
Never interpolate user data into a script block with {!! !!}, and don't
hand-build the object with {{ }} either — {{ }} escapes for HTML, not for
JavaScript, so a value containing </script> or a quote can break out of the
block. That's a stored XSS, delivered by your own analytics snippet. Use
@json() and let it build the whole object.
In plain PHP the equivalent is:
<script>
stepskit.identify(<?= json_encode([
'id' => (string) $user->id,
'email' => $user->email,
], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>);
</script>The JSON_HEX_* flags are what make it safe to emit inside <script>.
Or use data attributes
If you'd rather not emit a second script block, StepsKit reads any
data-user-* attribute off the script tag:
<script
src="https://cdn.stepskit.com/stepskit.latest.js"
data-api-key="YOUR_API_KEY"
@auth
data-user-id="{{ auth()->id() }}"
data-user-email="{{ auth()->user()->email }}"
data-user-plan="{{ auth()->user()->plan }}"
@endauth
async
></script>Here {{ }} is correct — these are HTML attribute values, not a JavaScript
context. This form skips the queue stub, so guard any later JavaScript calls
with window.stepskit?..
Pass whatever your user model already carries; every key becomes targetable and there's no fixed schema to conform to.
3. Verify it worked
Load any page of your app, then:
- Dashboard — the project's Integrations page flips to Connected.
- Console, on a signed-in page — run
window.stepskit.validateEnvironment(). You want a non-nullvisitorIdand a populateduserAttributes. A nullvisitorIdmeans step 2 never ran. - Network —
cdn.stepskit.com/stepskit.latest.jsreturns200.
validateEnvironment() also returns a toursFiltered table that says exactly
why any given tour isn't showing.
Laravel notes
Inertia and Livewire
Inertia turns your Laravel app into a single-page app. Attributes then do survive navigation, and you should identify from the client side instead — see the React or Vue guide depending on your Inertia adapter. The loader still goes in the Blade root layout.
Livewire swaps parts of the DOM in place. If a tour step is anchored to an element inside a Livewire component, that element can be replaced while the tour is open and the step will lose its anchor. Anchor tour steps to stable elements outside the swapped region where you can.
Content Security Policy
Laravel doesn't ship a CSP by default; if you've added one (commonly via
spatie/laravel-csp), StepsKit needs:
script-src:https://cdn.stepskit.com, plus'unsafe-inline'for the loader and identify blocks (not needed if you used the data-attribute form)connect-src:https://stepskit.comstyle-src:'unsafe-inline'— required. The embed injects its entire stylesheet as a<style>element when the script evaluates. Without this the tour engine renders completely unstyled rather than merely off-brand.img-src:https://*.supabase.co— only if your steps use images.
Troubleshooting
Dashboard still says "Not connected". The layout you edited isn't the one
that page extends. Check for more than one layout in resources/views/layouts/.
Connected, but tours only work on some pages. Same cause — some routes use a different layout.
Connected, but no tour plays anywhere. Usually identity. Run
validateEnvironment(): if visitorId is null, your @auth block isn't
rendering, or it's rendering above the loader rather than below it.
Show-once tours reappear on every visit. identify isn't running on every
page load. In an MPA it must be in the global layout, not on the login page.
Console syntax error inside the identify block. You interpolated a value
without @json(). See above — this is also a security bug, not just a syntax
one.
Tour renders as unstyled text. Your CSP is missing
style-src 'unsafe-inline'.
Next steps
- JavaScript API reference — every method on
window.stepskit. - User attributes — what you can target on.
- Frequency capping — how "show once" works.