# Add CLItrail to Server-rendered apps (Rails, Laravel, Django, Express)

Where the CLItrail tag goes in Server-rendered apps (Rails, Laravel, Django, Express), in its own idiom, with the consent, CSP and client-side routing details and how to check it works.

Written for Any version: the tag goes in the shared layout

## Where the tag goes

Edit `app/views/layouts/application.html.erb (Rails), resources/views/layouts/app.blade.php (Laravel), templates/base.html (Django), your views layout (Express)`. Put the tag on every page that shows your install command; site-wide is best, after your existing analytics tags.

```html
<head>
  <meta charset="utf-8" />
  <!-- …your analytics tags first -->
  <script defer src="https://YOUR_SERVICE/browser.js" data-website="YOUR_WEBSITE_ID"></script>
</head>
```

## Only after consent

Use the loader in place of the tag: nothing from CLItrail loads until `startClitrail()` runs. Load the loader before your consent manager's script and call `startClitrail()` from the manager's accept callback (most managers also run it on later visits once a choice is stored; check yours). Use the marketing category when installs go to Google Ads, Meta, TikTok or X Ads, and the analytics category when they only reach GA4 or your webhooks. In Django, point the src at `{% static 'clitrail-consent.js' %}`.

Consent loader

```js
// Loads CLItrail only after the visitor agrees. Call startClitrail() from your
// consent manager's "accepted" callback; nothing is fetched or stored before that.
window.startClitrail = () => {
  if (document.getElementById('clitrail')) return;
  const tag = document.createElement('script');
  tag.id = 'clitrail';
  tag.src = 'https://YOUR_SERVICE/browser.js';
  tag.dataset.website = 'YOUR_WEBSITE_ID';
  document.head.append(tag);
};
```

```html
<head>
  <meta charset="utf-8" />
  <!-- …your analytics tags first -->
  <script defer src="/clitrail-consent.js"></script>
</head>
```

## Client-side routing

Server-rendered pages load in full. With Hotwire Turbo (the Rails default), pages change without a reload: record them on `turbo:load`, which Turbo fires on the first load and after every visit. Laravel Livewire's `wire:navigate` fires `livewire:navigated` and Inertia's router fires `navigate`: use the same function on those.

```js
// Records Turbo visits; the first load is the page the tag already recorded.
(() => {
  let recorded;
  const recordVisit = () => {
    recorded ??= new URL(performance.getEntriesByType('navigation')[0]?.name ?? location.href).pathname;
    const sdk = window.InstallAttribution;
    if (!sdk?.ready || location.pathname === recorded) return;
    recorded = location.pathname;
    sdk.ready = sdk.init({ service: 'https://YOUR_SERVICE', project: 'YOUR_WEBSITE_ID' });
  };
  document.addEventListener('turbo:load', recordVisit);
})();
```

## Content Security Policy

Rails: `config/initializers/content_security_policy.rb` (`policy.script_src`, `policy.connect_src`, `policy.worker_src`). Laravel and Django: the CSP middleware or settings you use. Express: `helmet({ contentSecurityPolicy })` directives. In each, add `https://YOUR_SERVICE` to `script-src` and `connect-src`, and `blob:` to `worker-src` (only Safari before 26 needs that one).

```
script-src 'self' https://YOUR_SERVICE;
connect-src 'self' https://YOUR_SERVICE;
worker-src 'self' blob:;
```

## Verify it works

1. Start your app server locally (register its origin) or deploy.
2. Open a page with the tag in Chrome or Firefox (accept analytics first if you gate CLItrail on consent), open the developer console and run `await InstallAttribution.ready`. It resolves to `{ ok: true, urlId, hasAnalyticsContext, expiresAt }`.
3. In the CLItrail dashboard, open Visits & identities: the visit is listed.
4. Anything else names the cause: `disabled` (no consent yet, or `data-enabled="false"`), `opfs_unavailable` (the page is not served over https or from localhost), `Error` (the service refused the visit: add the page's origin, including a development origin such as `http://localhost:3000`, under Settings → Additional domains). If `InstallAttribution` is undefined, the tag did not load: check the Network tab and your Content-Security-Policy.

Then run your installer on the same computer and check **Install events**, or run the hook with [`--doctor`](https://clitrail.com/docs/doctor).

## Notes

- Put the tag in the one layout every public page uses, after your analytics tags.
- In ERB, Blade and Django templates the tag is plain HTML: no helper is needed.

Server-rendered apps (Rails, Laravel, Django, Express) documentation: [turbo.hotwired.dev/reference/events](https://turbo.hotwired.dev/reference/events)

Using other install paths or destinations? The [setup generator](https://clitrail.com/docs/setup-generator) puts this snippet together with your hooks and destination checklist.
