# Install StepsKit in Laravel or plain PHP

> Add StepsKit to a Laravel Blade layout or any PHP app. Covers safely escaping user data into the identify call, why every page load must re-identify, and Inertia and Livewire caveats.

*Source: https://stepskit.com/docs/install/laravel*

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](#inertia-and-livewire)? It behaves like a
single-page app; read that section.

## Prerequisites

- Your project API key, from the **Integrations** page in the
  [dashboard](/app).
- 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>`:

```html
<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:

```html
{{-- resources/views/layouts/app.blade.php --}}
    @stack('scripts')
  </body>
</html>
```

```html
{{-- any view --}}
@push('scripts')
  {{-- the StepsKit loader --}}
@endpush
```

**Plain 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:

```html
{{-- resources/views/layouts/app.blade.php --}}
@auth
  <script>
    stepskit.identify(@json([
      'id' => (string) auth()->id(),
      'email' => auth()->user()->email,
      'plan' => auth()->user()->plan,
    ]));
  </script>
@endauth
```

### Escape 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:

```php
<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:

```html
<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:

1. **Dashboard** — the project's Integrations page flips to **Connected**.
2. **Console, on a signed-in page** — run
   `window.stepskit.validateEnvironment()`. You want a non-null `visitorId` and
   a populated `userAttributes`. A null `visitorId` means step 2 never ran.
3. **Network** — `cdn.stepskit.com/stepskit.latest.js` returns `200`.

`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](/docs/install/react.md) or [Vue](/docs/install/vue.md) 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.com`
- `style-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](/docs/api.md) — every method on `window.stepskit`.
- [User attributes](/docs/concepts/user-attributes.md) — what you can target on.
- [Frequency capping](/docs/concepts/frequency-capping.md) — how "show once" works.
