> **Replace `{SITE_ID}` with your tracking ID** before pasting any snippet. You'll find it in your PerchLens dashboard under Settings → Tracking. Or fetch a personalised version of this guide at `/install.md?site=YOUR_TRACKING_ID`.

# PerchLens — Install Guide

PerchLens is a privacy-first web analytics service. Adding it to your site means putting one `<script>` tag on every page. This guide is structured so an AI assistant (Claude, ChatGPT, Cursor, Copilot, etc.) can:

1. Identify your stack from the project files.
2. Pick the correct variant below.
3. Insert the snippet in the exact file and location specified.
4. Verify it's working.

The script is **`https://perchlens.com/cv.js`**, **defer-loaded**, and reads its tracking ID from a `data-site` attribute. It's around 4 KB gzipped, sets no cookies, and never blocks render.

The minimum, framework-agnostic snippet:

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

Place it inside `<head>` (preferred) or just before `</body>` (acceptable). Every other variant in this document is just the same script wrapped to fit a specific framework's idioms.

---

## Table of Contents

- [Quick checklist](#quick-checklist)
- [Next.js](#nextjs)
- [Remix](#remix)
- [Astro](#astro)
- [SvelteKit](#sveltekit)
- [Nuxt 3](#nuxt-3)
- [Vue 3 (Vite)](#vue-3-vite)
- [React (Vite or Create React App)](#react-vite-or-create-react-app)
- [Gatsby](#gatsby)
- [Plain HTML / Static Site Generators](#plain-html--static-site-generators)
- [Hugo](#hugo)
- [Jekyll](#jekyll)
- [Eleventy (11ty)](#eleventy-11ty)
- [Framer](#framer)
- [Webflow](#webflow)
- [WordPress](#wordpress)
- [Shopify](#shopify)
- [Squarespace](#squarespace)
- [Wix](#wix)
- [Ghost](#ghost)
- [Google Tag Manager (GTM)](#google-tag-manager-gtm)
- [Cloudflare Pages / Workers (header injection)](#cloudflare-pages--workers-header-injection)
- [Custom events](#custom-events)
- [Identifying users](#identifying-users)
- [Self-hosting the script](#self-hosting-the-script)
- [Content Security Policy (CSP)](#content-security-policy-csp)
- [Verification](#verification)
- [Troubleshooting](#troubleshooting)

---

## Quick checklist

Before you ship:

1. The script tag includes `defer` (do not remove it).
2. `data-site` matches the tracking ID shown in your PerchLens dashboard.
3. The script is in `<head>` of every page that should be tracked.
4. Your CSP (if any) allows `https://perchlens.com`. See [CSP](#content-security-policy-csp).
5. Visit your live site once, then check the realtime view in your dashboard.

---

## Next.js

PerchLens supports both the App Router and the Pages Router. Pick the variant that matches your project.

### Next.js — App Router (Next.js 13+)

**File:** `app/layout.tsx` (or `app/layout.jsx`)
**Place:** Inside the root `<html>` element, in the `<head>` block. The `next/script` wrapper with `strategy="afterInteractive"` is the canonical way to load third-party scripts in the App Router — it ships after hydration and never competes with first paint.

```tsx
import Script from "next/script";

export default function RootLayout({ children }: { children: React.ReactNode }) {
    return (
        <html lang="en">
            <head>
                <Script
                    src="https://perchlens.com/cv.js"
                    data-site="{SITE_ID}"
                    strategy="afterInteractive"
                />
            </head>
            <body>{children}</body>
        </html>
    );
}
```

**Verify:** Deploy, then visit your live site once. The realtime counter on your PerchLens dashboard should tick within ~5 seconds.

### Next.js — Pages Router (legacy)

**File:** `pages/_document.tsx` (or `pages/_document.jsx`)
**Place:** Inside the `<Head>` component returned by `Document`. The Pages Router `<Head>` from `next/document` is required so the script is in the SSR'd HTML, not just the client tree.

```tsx
import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
    return (
        <Html lang="en">
            <Head>
                <script
                    defer
                    src="https://perchlens.com/cv.js"
                    data-site="{SITE_ID}"
                />
            </Head>
            <body>
                <Main />
                <NextScript />
            </body>
        </Html>
    );
}
```

**Verify:** As above.

---

## Remix

**File:** `app/root.tsx`
**Place:** Inside the `<head>` of the root `Layout` component, alongside `<Meta />` and `<Links />`.

```tsx
import { Meta, Links, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";

export default function App() {
    return (
        <html lang="en">
            <head>
                <Meta />
                <Links />
                <script
                    defer
                    src="https://perchlens.com/cv.js"
                    data-site="{SITE_ID}"
                />
            </head>
            <body>
                <Outlet />
                <ScrollRestoration />
                <Scripts />
            </body>
        </html>
    );
}
```

**Verify:** Remix client-side navigations dispatch `pushState` — the tracker auto-detects this and registers a pageview per route change.

---

## Astro

**File:** `src/layouts/Layout.astro` (or whichever layout wraps every page)
**Place:** Inside the `<head>` of the layout. Add `is:inline` so the tag survives Astro's island serialisation and isn't bundled.

```astro
---
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>{title}</title>
        <script is:inline defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
    </head>
    <body>
        <slot />
    </body>
</html>
```

**Note:** If you use `<ViewTransitions />` from `astro:transitions`, no extra setup needed — the tracker hooks into `astro:page-load` automatically.

**Verify:** Build and preview locally with `npm run preview`, navigate between pages, then check realtime.

---

## SvelteKit

**File:** `src/app.html`
**Place:** Inside the `<head>`, alongside the existing meta tags and `%sveltekit.head%` placeholder.

```html
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
        %sveltekit.head%
    </head>
    <body data-sveltekit-preload-data="hover">
        <div style="display: contents">%sveltekit.body%</div>
    </body>
</html>
```

**Verify:** SvelteKit's client-side navigation triggers `pushState`; the tracker registers a pageview per route.

---

## Nuxt 3

**File:** `nuxt.config.ts`
**Place:** Add the script under the `app.head.script` array. Nuxt injects it into the SSR'd `<head>`.

```ts
export default defineNuxtConfig({
    app: {
        head: {
            script: [
                {
                    src: "https://perchlens.com/cv.js",
                    "data-site": "{SITE_ID}",
                    defer: true,
                },
            ],
        },
    },
});
```

**Verify:** Run `npm run build && npm run preview`, navigate between pages.

---

## Vue 3 (Vite)

**File:** `index.html`
**Place:** Inside `<head>`, after the title.

```html
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>My App</title>
        <script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
    </head>
    <body>
        <div id="app"></div>
        <script type="module" src="/src/main.ts"></script>
    </body>
</html>
```

**Note:** If you're using Vue Router in history mode, the tracker auto-detects `pushState` navigation. No additional configuration needed.

---

## React (Vite or Create React App)

**File:**
- Vite: `index.html`
- CRA: `public/index.html`

**Place:** Inside `<head>`, after the title and meta tags.

```html
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>My App</title>
        <script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
    </head>
    <body>
        <div id="root"></div>
    </body>
</html>
```

**Note:** React Router `pushState` navigations are auto-tracked.

---

## Gatsby

**File:** `gatsby-ssr.js` (create at project root if it doesn't exist)
**Place:** Use the `onRenderBody` API to inject the script in `<head>`.

```js
import React from "react";

export const onRenderBody = ({ setHeadComponents }) => {
    setHeadComponents([
        <script
            key="perchlens"
            defer
            src="https://perchlens.com/cv.js"
            data-site="{SITE_ID}"
        />,
    ]);
};
```

**Verify:** Build with `gatsby build && gatsby serve`, navigate between pages.

---

## Plain HTML / Static Site Generators

**File:** Whatever your base layout/template is. Examples: `index.html`, `_layouts/default.html`, `themes/yourtheme/layouts/_default/baseof.html`.
**Place:** Inside `<head>`, before `</head>`.

```html
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>My Site</title>
        <script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
    </head>
    <body>
        <!-- your content -->
    </body>
</html>
```

**Verify:** Open your site in a private/incognito window so adblockers in your normal browser session don't interfere.

---

## Hugo

**File:** `layouts/_default/baseof.html` (or `layouts/partials/head.html` if your theme uses a partial)
**Place:** Inside the `<head>` block.

```html
<!doctype html>
<html lang="{{ .Site.LanguageCode }}">
    <head>
        <meta charset="utf-8" />
        <title>{{ .Title }} | {{ .Site.Title }}</title>
        <script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
    </head>
    <body>
        {{ block "main" . }}{{ end }}
    </body>
</html>
```

---

## Jekyll

**File:** `_includes/head.html` (most themes have this; if not, edit `_layouts/default.html`)
**Place:** Inside the `<head>` include.

```html
<head>
    <meta charset="utf-8" />
    <title>{{ page.title }}</title>
    <script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
</head>
```

---

## Eleventy (11ty)

**File:** `_includes/layouts/base.njk` (or `base.html`, depending on template engine)
**Place:** Inside the `<head>` block.

```njk
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>{{ title }}</title>
        <script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
    </head>
    <body>
        {{ content | safe }}
    </body>
</html>
```

---

## Framer

**No file editing required.** Use the built-in Custom Code panel.

1. Open your Framer project.
2. Click the gear icon in the top toolbar → **Site Settings**.
3. Scroll to the **General** tab → **Custom Code** section.
4. In the **Start of `<head>` tag** text area, paste:

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

5. Click **Publish** in the top-right.

**Note:** Custom Code is only available on Framer paid plans (Mini and above).

---

## Webflow

**No file editing required.** Use the Custom Code panel.

1. Webflow Designer → click the project name (top-left) → **Project Settings**.
2. Open the **Custom Code** tab.
3. In the **Head Code** editor, paste:

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

4. **Save Changes**, then publish your site.

**Note:** Custom Code requires a paid Webflow Site Plan (Basic or higher).

---

## WordPress

PerchLens supports both plugin-based installation (recommended) and direct theme editing.

### WordPress — via plugin (recommended)

1. Install **Insert Headers and Footers** by WPBeginner, **WPCode**, or **Header and Footer Scripts**.
2. Open the plugin's settings (usually under **Settings**).
3. In the **Scripts in Header** field, paste:

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

4. Save.

### WordPress — via child theme

**File:** `wp-content/themes/your-child-theme/functions.php`
**Place:** Add to the bottom of the file (or any non-conditional spot).

```php
<?php
add_action('wp_head', function () {
    echo '<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>';
});
```

**Warning:** Edit a **child theme**, never the parent theme. Updating the parent theme would erase your changes.

---

## Shopify

PerchLens lives in the storefront and (on Shopify Plus) the checkout flow.

### Shopify — Storefront

**File:** `layout/theme.liquid`
**Place:** Just before the closing `</head>` tag.

1. Shopify admin → **Online Store** → **Themes** → click **Actions** next to your live theme → **Edit code**.
2. In the left file tree, expand **Layout** → click `theme.liquid`.
3. Find `</head>` and add the script just before it:

```liquid
    <script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
</head>
```

4. Click **Save**.

### Shopify — Checkout (Shopify Plus only)

**File:** Settings → **Checkout** → **Order status page** → **Additional scripts**
**Place:** Paste the same snippet as above. This requires Shopify Plus; Basic plans cannot inject scripts into the checkout flow.

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

---

## Squarespace

1. **Settings** → **Advanced** → **Code Injection**.
2. In the **HEADER** box, paste:

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

3. Click **Save**.

**Note:** Code Injection requires a Squarespace Business plan or higher.

---

## Wix

1. Open your Wix dashboard → **Settings** → **Custom Code** (under Advanced).
2. Click **+ Add Custom Code**.
3. In the code box, paste:

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

4. Set **Add Code to Pages** = **All pages**.
5. Set **Place Code in** = **Head**.
6. **Apply**.

**Note:** Custom Code requires a Wix premium plan with a connected domain.

---

## Ghost

1. Ghost admin → **Settings** → **Code Injection**.
2. In the **Site Header** field, paste:

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

3. Click **Save**.

---

## Google Tag Manager (GTM)

Use this when you want to manage analytics scripts centrally and don't want developer involvement for changes.

1. **Tags** → **New** → **Tag Configuration** → **Custom HTML**.
2. Paste into the **HTML** field:

```html
<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>
```

3. **Triggering** → **All Pages**.
4. **Save** the tag, then **Submit** + **Publish** the container.

**Note:** GTM adds a small latency vs direct `<head>` injection. If you can edit the site directly, that's faster. If you can't, GTM works fine.

---

## Cloudflare Pages / Workers (header injection)

If you can't edit the source HTML (e.g. you're behind Cloudflare in front of a third-party origin), you can inject the script via an HTMLRewriter Worker.

**File:** `functions/_middleware.ts` (Cloudflare Pages Functions) or your Worker entry point
**Place:** Standard worker module export.

```ts
export const onRequest: PagesFunction = async ({ next }) => {
    const response = await next();
    if (!response.headers.get("content-type")?.includes("text/html")) {
        return response;
    }
    return new HTMLRewriter()
        .on("head", {
            element(el) {
                el.append(
                    `<script defer src="https://perchlens.com/cv.js" data-site="{SITE_ID}"></script>`,
                    { html: true },
                );
            },
        })
        .transform(response);
};
```

**Verify:** Hit any HTML page on your domain, view source, confirm the tag appears.

---

## Custom events

Track conversions and custom interactions by adding `data-track` attributes to elements:

```html
<!-- A click event -->
<button data-track="signup-click">Sign up</button>

<!-- A form submission -->
<form data-track="contact-form-submit">...</form>

<!-- An outbound link -->
<a data-track="docs-cta" href="https://example.com">Docs</a>
```

The tracker fires the named event automatically when the element is clicked or the form is submitted.

For programmatic events:

```html
<script>
    // Fire a custom event from anywhere in your code
    window.cv?.track("checkout-started", { plan: "pro", amount: 9 });
</script>
```

The second argument is an optional properties object. Keep keys short and avoid storing personal data — properties are visible in the dashboard exactly as sent.

---

## Identifying users

PerchLens does **not** support identifying individual visitors by user ID, email, or any other personal identifier. This is a deliberate privacy choice. You can attach context to events via custom-event properties (e.g. `{ plan: "pro" }`), but never identifiers tied to a person.

If you need cohort analysis (e.g. "Pro users who completed checkout"), pass `plan` as a property on the relevant events and segment in the dashboard.

---

## Self-hosting the script

Recommended for sites with strict CSP policies that disallow third-party origins.

1. Download the current script:

```bash
curl -O https://perchlens.com/cv.js
```

2. Serve it from your own domain (e.g. `/static/cv.js`).
3. Update the snippet to point at your copy:

```html
<script defer src="/static/cv.js" data-site="{SITE_ID}"></script>
```

**Important:** You're responsible for updating the local copy when we ship a new version. Subscribe to the changelog at `https://perchlens.com/changelog` for updates.

---

## Content Security Policy (CSP)

If your site uses CSP, add the following directives:

```http
script-src 'self' https://perchlens.com;
connect-src 'self' https://perchlens.com;
```

If you're self-hosting the script, you only need `connect-src https://perchlens.com` (so the tracker can POST events to our ingest endpoint).

For `Content-Security-Policy-Report-Only` mode, monitor for blocks and migrate to enforcement once clear.

---

## Verification

After installing, verify the script is working:

1. Open your live site in a private/incognito window.
2. Open DevTools → **Network** tab.
3. Reload the page. You should see two requests:
   - `GET https://perchlens.com/cv.js` → status 200
   - `POST https://perchlens.com/api/collect` → status 202
4. Open your PerchLens dashboard → **Realtime**. You should appear within ~5 seconds.

If you don't see the `/api/collect` request:

- The script tag might be inside `<body>` instead of `<head>`. Move it.
- An adblocker may be blocking it. Test in private window with no extensions.
- Your CSP may be blocking it. See [CSP](#content-security-policy-csp).

---

## Troubleshooting

### "I installed the script but no pageviews show up"

1. Verify the `data-site` value matches the tracking ID in your dashboard exactly. No surrounding whitespace.
2. Confirm `/cv.js` loads in DevTools Network tab.
3. Confirm `/api/collect` POSTs are firing (status 202).
4. Check if your domain matches your site's "Allowed domains" filter in dashboard Settings (if you've set any).

### "Pageviews appear from my own browser but not from real visitors"

You may have configured a country or IP block. Settings → **Filters** → review the block lists.

### "I see double pageviews"

You've probably installed the script twice. Common culprits:

- The same snippet in `theme.liquid` AND a section file (Shopify).
- The snippet in `_layout.html` AND `_includes/head.html` (Jekyll).
- A copy-paste duplicate inside the same `<head>`.

Search your codebase for `https://perchlens.com/cv.js` and remove duplicates.

### "SPA navigations aren't tracked"

The tracker auto-detects `pushState` and `popstate`. If your framework uses a non-standard navigation API (rare), call `window.cv?.pageview()` manually after each route change.

### "The script breaks when I bundle it through my build"

Do **not** import the script through your bundler. It's designed to be loaded by the browser via a `<script>` tag. Bundling it strips the `data-site` attribute and breaks the tracking ID lookup.

---

## Live verification endpoint

`GET https://perchlens.com/api/sites/verify-tracking?site_id={SITE_ID}` returns:

```json
{
    "verified": true,
    "pageviews": 12,
    "visitors": 4
}
```

You can poll this endpoint from your build pipeline to confirm installation before promoting to production.

---

*Generated from `src/lib/install-md.ts`. Source: `https://perchlens.com/install.md`. For platform-specific landing pages with screenshots, see `https://perchlens.com/docs/install`.*
