# Install StepsKit on WordPress

> Add StepsKit to WordPress with wp_enqueue_script from a site-specific plugin, add the data-api-key attribute via script_loader_tag, and identify logged-in users with wp_add_inline_script.

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

Two paths below: a proper `wp_enqueue_script` install for developers, and a
header-and-footer-scripts plugin for everyone else. Both work; the first
survives theme changes.

**A note on fit.** WordPress "users" are usually admins, editors and authors —
not product users. StepsKit is most useful here on a membership site, a
LearnDash/WooCommerce customer area, or inside `wp-admin` to onboard editors. On
a purely anonymous marketing site you can still run tours, but you won't get
per-user targeting or reliable show-once capping.

## Prerequisites

- Your project API key, from the **Integrations** page in the
  [dashboard](/app).
- Ability to add a plugin, or edit a child theme's `functions.php`.

## Don't paste into `header.php`

The tempting move is to open Appearance → Theme File Editor and drop the snippet
into the active theme's `header.php` or `footer.php`. Don't:

- it's lost the next time the theme updates,
- it's lost if you switch themes,
- and it's invisible to whoever inherits the site.

Use one of the two options below instead.

## 1. Add the loader

### Option A — a site-specific plugin (recommended)

Create `wp-content/plugins/stepskit/stepskit.php`:

```php
<?php
/**
 * Plugin Name: StepsKit
 * Description: Loads the StepsKit product tour SDK.
 */

if (!defined('ABSPATH')) {
    exit;
}

const STEPSKIT_API_KEY = 'YOUR_API_KEY';

add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'stepskit',
        'https://cdn.stepskit.com/stepskit.latest.js',
        [],
        null,   // no ?ver= query string on a CDN URL
        true    // load in the footer
    );
});

// wp_enqueue_script cannot set arbitrary attributes, so add data-api-key here.
add_filter('script_loader_tag', function ($tag, $handle) {
    if ($handle !== 'stepskit') {
        return $tag;
    }

    return str_replace(
        ' src=',
        ' async data-api-key="' . esc_attr(STEPSKIT_API_KEY) . '" src=',
        $tag
    );
}, 10, 2);
```

Activate it under Plugins. A plugin rather than a child theme because it keeps
working when the site is re-themed.

Two details worth knowing:

- **`null` for `$ver`** omits the `?ver=6.x` query string WordPress otherwise
  appends. On a CDN URL that only fragments caching.
- **`script_loader_tag` is the only way to add `data-api-key`.**
  `wp_enqueue_script` has no parameter for arbitrary attributes — the newer
  `$args` array covers `strategy` (`async`/`defer`) and `in_footer`, not data
  attributes.

### Option B — a header/footer scripts plugin

If you'd rather not write PHP, install any "insert headers and footers" plugin
and paste the [HTML snippet](/docs/install/js.md) into the **footer** field. It
works, and it survives theme updates because the plugin owns it.

You'll still need Option A's approach — or that plugin's own PHP hooks — to
identify logged-in users.

## 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.

WordPress is a multi-page app, so **user attributes don't survive a page load**
— the identify call has to run on every request, not just after login. Attaching
it to the enqueued handle does exactly that.

Add this to the same plugin, inside the `wp_enqueue_scripts` action, after the
`wp_enqueue_script` call:

```php
if (is_user_logged_in()) {
    $user = wp_get_current_user();

    wp_add_inline_script('stepskit', sprintf(
        'window.stepskit = window.stepskit || {}; window.stepskit._q = window.stepskit._q || []; window.stepskit._q.push(["identify", %s]);',
        wp_json_encode([
            'id'    => (string) $user->ID,
            'email' => $user->user_email,
            'plan'  => implode(',', $user->roles),
        ])
    ), 'after');
}
```

Pushing onto `_q` rather than calling `window.stepskit.identify()` directly is
deliberate: `wp_add_inline_script(..., 'after')` prints the block immediately
after the `<script src>` tag, which is *before* the async SDK has finished
downloading. The queue is what StepsKit drains on startup, so the call survives
the gap.

`wp_json_encode` is what makes this safe to emit inside a `<script>` block —
never build the object by concatenating fields.

Swap `plan` for whatever you actually want to target on. On a membership site
that's usually the membership level rather than the WordPress role.

## 3. Verify it worked

Load any page of your site while logged in, then:

1. **Dashboard** — the project's Integrations page flips to **Connected**.
2. **Console** — 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.

## WordPress notes

**Running tours inside `wp-admin`.** `wp_enqueue_scripts` only fires on the
front end. To onboard editors in the admin, hook `admin_enqueue_scripts` as well
(or instead) — same enqueue call, same filter.

**Caching plugins.** WP Rocket, LiteSpeed Cache and friends can defer, combine
or delay third-party scripts. If tours stop firing after you enable one, exclude
`cdn.stepskit.com` from JavaScript optimisation. This is the most common cause
of "it worked yesterday".

**Block themes and headless WordPress.** A block theme enqueues the same way.
Headless WordPress is a different install — follow the guide for whatever
framework renders the front end.

**Content Security Policy.** WordPress doesn't set one by default; if a security
plugin does, StepsKit needs:

- `script-src`: `https://cdn.stepskit.com`, plus `'unsafe-inline'` for the
  inline identify block
- `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".** Check the plugin is activated, and
view source for `cdn.stepskit.com`. If the tag is missing, another plugin is
dequeuing it.

**Script tag is present but has no `data-api-key`.** Your `script_loader_tag`
filter isn't matching. The `$handle` must be exactly the string you passed to
`wp_enqueue_script` — `'stepskit'` in the example above.

**Connected, but no tour plays.** Usually identity. Run
`validateEnvironment()`: a null `visitorId` means the inline script isn't
rendering — confirm `is_user_logged_in()` is true on that page.

**Worked, then stopped after installing a cache plugin.** Exclude
`cdn.stepskit.com` from JS optimisation. See above.

**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.
