---
name: highseat
description: Install Highseat web analytics on the website in this project, with the site id the user gives. Use when asked to add Highseat, its tracking script or the highseat npm package to a site.
---

# Install Highseat analytics

These steps add Highseat to the website in the current project. The user's prompt carries the site id, which starts with `site_`. Write it wherever the code below shows `YOUR_SITE_ID`.

## Rules

- If the site already loads Highseat, skip section 2 and tell the user so. It does when the code renders `<Highseat`, calls `inject(` from `highseat`, or a page has a script tag with a `data-site` attribute holding a `site_` id. A `highseat` dependency alone is not an install: finish the steps. When `HIGHSEAT_PATH` appears in the project, the proxy is set up too, so skip section 4. A second run therefore changes nothing.
- Without a site id in the prompt, ask the user for it. The id is public, and Installation settings in the Highseat dashboard shows it. Never make one up.
- Never ask the user to paste a secret into the chat, and never write one into a file git tracks. Code reads `HIGHSEAT_SECRET` from the environment. Put it in a file `.gitignore` covers, such as `.env.local`, or tell the user which host setting it goes in. Installation settings creates the secret.
- Follow the steps for one framework. Install the package with the project's own package manager: `bun add`, `pnpm add` or `yarn add` in place of `npm install`.
- Paths start at the project root. When the project keeps a file elsewhere, such as `src/app/layout.tsx` in a Next.js app with a `src` folder, edit that file.
- Keep the project's code style, and merge each file below into the existing file.

## 1. Find the framework

Take the first row that matches.

| Framework | How to tell |
| --- | --- |
| Next.js | `next` is a dependency in `package.json`. |
| TanStack Start | `@tanstack/react-start` is a dependency. |
| Astro | `astro` is a dependency. |
| React | `react` and `vite` are dependencies. |
| WordPress | The project has `wp-config.php` or a `wp-content` folder. |
| Any other site | Anything else, such as a folder of static HTML pages. |

## 2. Install

This install loads the tracker from Highseat. It needs no secret and no server code.

### Next.js

1. Install the package.

```bash
npm install highseat
```

2. Add it to your root layout. File: `app/layout.tsx`.

```tsx
import { Highseat } from "highseat/next"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Highseat site="YOUR_SITE_ID" />
      </body>
    </html>
  )
}
```

### TanStack Start

1. Install the package.

```bash
npm install highseat
```

2. Add it to your root route. Inside <body>, in the root route’s component. File: `src/routes/__root.tsx`.

```tsx
import { Highseat } from "highseat/react"

<Highseat site="YOUR_SITE_ID" />
```

### Astro

1. Add it to your layout. In the <head>. is:inline keeps Astro from bundling it, so the tracker starts before any page script. File: `src/layouts/Layout.astro`.

```astro
<script is:inline defer src="https://cdn.highseat.com/script.js" data-site="YOUR_SITE_ID"></script>
```

### React

1. Install the package.

```bash
npm install highseat
```

2. Render it once, beside your root component.

```tsx
import { Highseat } from "highseat/react"

<Highseat site="YOUR_SITE_ID" />
```

### WordPress

1. Copy your tracking tag.

```html
<script defer src="https://cdn.highseat.com/script.js" data-site="YOUR_SITE_ID"></script>
```

2. Paste it into WordPress. Your theme’s header.php just before </head>, or a header scripts plugin such as WPCode, then Save.

### Any other site

1. Add it to every page. In the <head>, so it loads on every page of the site.

```html
<script defer src="https://cdn.highseat.com/script.js" data-site="YOUR_SITE_ID"></script>
```

## 3. Check the install

Git usually ignores `.env` and `.env.local`, so the deployed site never sees them. Tell the user to set every variable from the install's `.env` step in the host's environment, such as the project settings on Vercel, Netlify or Cloudflare, before the next deploy. The build writes the site id into the page, so a variable set after a build takes a new deploy.

Highseat refuses events from `localhost` and from any host the site does not allow, so the check needs the deployed site. Once it is deployed, the user opens the site in a browser, then Installation settings in the dashboard. It shows "First pageview received" once the first event arrives.

In development, the package logs every `track()` call to the browser console, and warns when the tracker script failed to load.

## 4. Optional: count visitors with ad blockers

Do this only when the user asks. An ad blocker can stop a script from Highseat's domain. The proxy serves the tracker from a path on the site's own domain, so blockers let it through. It needs two values from Installation settings: the proxy path, written `/YOUR_PROXY_PATH` below, and the site secret.

Pick the runtime that serves the site. Its steps include the install, so follow them in place of section 2, and on a site that has section 2 already, change only what differs.

### Next.js on Vercel

A Next.js app. Off Vercel, pick the `clientIp` preset the route's step names.

1. Install the package.

```bash
npm install highseat
```

2. Add the environment variables. In .env.local, and in your host’s environment for the build as well as the server: the build writes the rewrite.

```dotenv
NEXT_PUBLIC_HIGHSEAT_SITE=YOUR_SITE_ID
HIGHSEAT_PATH=/YOUR_PROXY_PATH
HIGHSEAT_SECRET=hs_secret_...
```

3. Add the proxy route. “vercel” reads the IP Vercel sets. Behind a proxied Cloudflare record, use “cloudflare” on any host; behind nginx alone, “nginx”. File: `app/api/highseat/route.ts`.

```ts
import { createProxy } from "highseat/next"

const proxy = createProxy({ clientIp: "vercel" })

export { proxy as GET, proxy as POST }
```

4. Wrap your Next.js config. Rewrites the proxy path to the route, ahead of every other route. File: `next.config.ts`.

```ts
import { withHighseat } from "highseat/next"

export default withHighseat({
  // Your own config.
})
```

5. Load the tracker from the path. In the root layout, in place of whatever loads the tracker now. Without src, it loads from HIGHSEAT_PATH. File: `app/layout.tsx`.

```tsx
import { Highseat } from "highseat/next"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Highseat />
      </body>
    </html>
  )
}
```

### TanStack Start

A TanStack Start app on any host.

1. Install the package.

```bash
npm install highseat
```

2. Add the environment variables. In .env. Set HIGHSEAT_PATH for the build as well as the server, as a build argument in a Docker build.

```dotenv
VITE_HIGHSEAT_SITE=YOUR_SITE_ID
HIGHSEAT_PATH=/YOUR_PROXY_PATH
HIGHSEAT_SECRET=hs_secret_...
```

3. Add the proxy to your server entry. Answers the proxy path and hands every other request to the app. A file route named after the path does not work: a catch-all route such as $id matches it first. “cloudflare” assumes a proxied Cloudflare record in front of the site; behind nginx alone, use “nginx”. File: `src/server.ts`.

```ts
import handler, { createServerEntry } from "@tanstack/react-start/server-entry"
import { createProxy } from "highseat/proxy"

const proxy = createProxy({
  clientIp: "cloudflare",
  fallback: request => handler.fetch(request),
})

export default createServerEntry({ fetch: request => proxy(request) })
```

4. Give the browser the same path. File: `vite.config.ts`.

```ts
import { defineConfig, loadEnv } from "vite"

export default defineConfig(({ mode }) => ({
  // Loads HIGHSEAT_PATH alone, so the site secret beside it never reaches the bundle.
  define: {
    "import.meta.env.HIGHSEAT_PATH": JSON.stringify(
      loadEnv(mode, process.cwd(), "HIGHSEAT_PATH").HIGHSEAT_PATH,
    ),
  },
  // Your own config.
}))
```

5. Load the tracker from the path. File: `src/routes/__root.tsx`.

```tsx
<Highseat site={import.meta.env.VITE_HIGHSEAT_SITE} src={import.meta.env.HIGHSEAT_PATH} />
```

### Astro

An Astro site with a server adapter.

1. Install the package.

```bash
npm install highseat
```

2. Add the environment variables. In .env. The file name carries the path, so the endpoint needs only the secret.

```dotenv
PUBLIC_HIGHSEAT_SITE=YOUR_SITE_ID
HIGHSEAT_SECRET=hs_secret_...
```

3. Add the proxy endpoint. Needs a server adapter. A static site uses its host’s function instead, such as Cloudflare Pages. On the Cloudflare adapter, pass secret: locals.runtime.env.HIGHSEAT_SECRET. clientAddress is the address that connected, so behind a proxied Cloudflare record, use “cloudflare” instead. File: `src/pages/YOUR_PROXY_PATH.ts`.

```ts
import type { APIRoute } from "astro"
import { createProxy } from "highseat/proxy"

export const prerender = false

export const ALL: APIRoute = ({ request, clientAddress }) =>
  createProxy({ clientIp: () => clientAddress })(request)
```

4. Load the tracker from the path. In your layout’s <head>, in place of the tag that loads from Highseat. File: `src/layouts/Layout.astro`.

```astro
<script is:inline defer src="/YOUR_PROXY_PATH" data-site={import.meta.env.PUBLIC_HIGHSEAT_SITE}></script>
```

### Cloudflare Worker

A site behind a Cloudflare Worker routed on its domain.

1. Install the package.

```bash
npm install highseat
```

2. Add the environment variables. In .dev.vars for a local run. For the deployed Worker, set the secret with wrangler secret put HIGHSEAT_SECRET and the path under vars in your Wrangler config.

```dotenv
HIGHSEAT_PATH=/YOUR_PROXY_PATH
HIGHSEAT_SECRET=hs_secret_...
```

3. Add the Worker. Route the Worker on yoursite.com/*: fallback: fetch passes every other request on to your origin.

```ts
import { createProxy } from "highseat/proxy"

interface Env {
  HIGHSEAT_PATH: string
  HIGHSEAT_SECRET: string
}

export default {
  fetch(request: Request, env: Env) {
    const proxy = createProxy({
      path: env.HIGHSEAT_PATH,
      secret: env.HIGHSEAT_SECRET,
      clientIp: "cloudflare",
      fallback: fetch,
    })
    return proxy(request)
  },
}
```

4. Load the tracker from the path. In the <head> of every page, in place of the tag that loads from Highseat.

```html
<script defer src="/YOUR_PROXY_PATH" data-site="YOUR_SITE_ID"></script>
```

### Cloudflare Pages

A static site on Cloudflare Pages, Astro without an adapter included.

1. Install the package.

```bash
npm install highseat
```

2. Add the environment variables. In the Pages project’s settings, as variables of the production environment. Encrypt the secret.

```dotenv
HIGHSEAT_PATH=/YOUR_PROXY_PATH
HIGHSEAT_SECRET=hs_secret_...
```

3. Add the proxy function. Pages ignores functions/ when the build ships a _worker.js, as the Astro, SvelteKit and Remix adapters do: remove the adapter from a static site. File: `functions/[path].ts`.

```ts
import { createProxy } from "highseat/proxy"

interface Env {
  HIGHSEAT_PATH: string
  HIGHSEAT_SECRET: string
}

export const onRequest: PagesFunction<Env> = ({ request, env, next }) => {
  const proxy = createProxy({
    path: env.HIGHSEAT_PATH,
    secret: env.HIGHSEAT_SECRET,
    clientIp: "cloudflare",
    fallback: next,
  })
  return proxy(request)
}
```

4. Run the function on the path alone. In the folder your build copies to its output as is, such as public/. Every page stays a static asset. File: `public/_routes.json`.

```json
{ "version": 1, "include": ["/YOUR_PROXY_PATH"], "exclude": [] }
```

5. Load the tracker from the path. In the <head> of every page, in place of the tag that loads from Highseat.

```html
<script defer src="/YOUR_PROXY_PATH" data-site="YOUR_SITE_ID"></script>
```

### nginx

A site nginx serves, with no functions.

1. Create the site secret. nginx reads no environment variables, so the secret goes into highseat-proxy.conf in place of hs_secret_.... Keep the file out of version control.

```dotenv
HIGHSEAT_SECRET=hs_secret_...
```

2. Add the proxy locations. In your server block.

```nginx
# A GET on the path loads the script; a POST goes to the events location below.
location = /YOUR_PROXY_PATH {
    if ($request_method = POST) {
        rewrite ^ /YOUR_PROXY_PATH/events last;
    }
    proxy_pass https://cdn.highseat.com/script.js;
    include highseat-proxy.conf;
}

location = /YOUR_PROXY_PATH/events {
    internal;
    proxy_pass https://cdn.highseat.com/api/event;
    include highseat-proxy.conf;
}
```

3. Add the headers they share. Beside your nginx config. File: `highseat-proxy.conf`.

```nginx
proxy_set_header Host cdn.highseat.com;
proxy_ssl_server_name on;
proxy_set_header X-Hs-Secret "hs_secret_...";
# Behind Cloudflare or another edge, use $http_cf_connecting_ip instead.
proxy_set_header X-Hs-Client-Ip $remote_addr;
# An empty value keeps the visitor's own header from reaching Highseat.
proxy_set_header Cookie "";
proxy_set_header Authorization "";
proxy_set_header Referer "";
```

4. Load the tracker from the path. In the <head> of every page, in place of the tag that loads from Highseat.

```html
<script defer src="/YOUR_PROXY_PATH" data-site="YOUR_SITE_ID"></script>
```

## 5. Optional: note the setup for later agents

For an install with the package, when the project has an `AGENTS.md` or `CLAUDE.md` with no `## Analytics` section, add this one, so later agents track events the same way:

```md
## Analytics

Highseat tracks this site through the `highseat` package. Send a custom event with `track()` from `highseat`. Declare every event name and its properties in `src/analytics.d.ts`, so a misspelt name fails the type check, and add a new event there before the first `track()` call that sends it.
```

Create `src/analytics.d.ts` only when no file in the project declares `Register` for `highseat`. List the events the user named, or leave the example commented out:

```ts
declare module "highseat" {
  interface Register {
    events: {
      // One line per event: its name, then its properties.
      // invite_sent: { role: "admin" | "member" }
    }
    strict: true
  }
}
```
