[invisit](https://invisit.dev) · [Docs](https://invisit.dev/docs) · [llms.txt](https://invisit.dev/llms.txt) · [llms-full.txt](https://invisit.dev/llms-full.txt)

# Web frameworks

> Where the tracker goes in Next.js, TanStack Start, Nuxt, SvelteKit, Astro, Remix, Waku, Vite, Angular, WordPress, Shopify, Framer and more. One line, once.

Source: https://invisit.dev/docs/web/frameworks

There are no wrapper packages: call `init()` from [`@invisitdev/web`](https://invisit.dev/docs/web/installation.md#npm-package) wherever your framework runs code once in the browser. Each snippet shows only what to add to the file your scaffold generated; keep the rest of it.

The [script tag](https://invisit.dev/docs/web/installation.md#script-tag) works in every framework too: put it in the root layout's `<head>`. It is the same tracker, and it follows client-side navigation on its own. The package is the better fit when you want `track()`, `identify()` and `consent()` as typed imports.

Hosted site builders (WordPress, Shopify, Framer) have no build step you control, so they take the script tag. Their sections are at the end of this page.

To also serve the tracker from your own domain and record crawler reads, add the server side from the [server frameworks](https://invisit.dev/docs/server/frameworks.md).

## Next.js

A client component rendered inside `<body>` in the root layout (App Router):

```tsx title="app/analytics.tsx"
'use client'
import { useEffect } from 'react'
import { init } from '@invisitdev/web'

export function Analytics() {
  useEffect(() => {
    init({ site: process.env.NEXT_PUBLIC_INVISIT_SITE! })
  }, [])
  return null
}
```

```tsx title="app/layout.tsx"
import { Analytics } from './analytics'

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

With the Pages Router, the same `useEffect` goes in `pages/_app.tsx`.

## TanStack Start

A component that starts the tracker once it is in the browser:

```tsx title="src/components/analytics.tsx"
import { useEffect } from 'react'
import { init } from '@invisitdev/web'

export function Analytics() {
  useEffect(() => {
    init({ site: import.meta.env.VITE_INVISIT_SITE })
  }, [])
  return null
}
```

Render `<Analytics />` beside `<Scripts />` in the `RootDocument` of `src/routes/__root.tsx`, so every route has it.

## Nuxt

A client-only plugin. Nuxt 4 reads `app/plugins/`; on Nuxt 3 the folder is `plugins/`.

```ts title="app/plugins/invisit.client.ts"
import { init } from '@invisitdev/web'

export default defineNuxtPlugin(() => {
  init({ site: useRuntimeConfig().public.invisitSite })
})
```

## SvelteKit

In the root layout. On SvelteKit 2, import `browser` from `$app/environment` instead.

```svelte title="src/routes/+layout.svelte"
<script lang="ts">
  import { browser } from '$app/env'
  import { init } from '@invisitdev/web'

  let { children } = $props()

  if (browser) init({ site: '<site id>' })
</script>

{@render children()}
```

## Astro

A `<script>` in your base layout. Astro bundles it and runs it in the browser.

```astro title="src/layouts/Base.astro"
<script>
  import { init } from '@invisitdev/web'
  init({ site: import.meta.env.PUBLIC_INVISIT_SITE })
</script>
```

## React Router and Remix

The same `useEffect` as for Next.js goes in the default `App` export of `app/root.tsx`. Keep `Layout`, `links` and `ErrorBoundary`: `Layout` renders `<Scripts />`, without which the page never hydrates.

## Waku

A client component, rendered by the root layout. Waku hands client components only variables named `WAKU_PUBLIC_*`.

```tsx title="src/components/analytics.tsx"
'use client'
import { useEffect } from 'react'
import { init } from '@invisitdev/web'

export function Analytics() {
  useEffect(() => {
    init({ site: import.meta.env.WAKU_PUBLIC_INVISIT_SITE })
  }, [])
  return null
}
```

```tsx title="src/pages/_layout.tsx"
import type { ReactNode } from 'react'
import { Analytics } from '../components/analytics'

export default async function RootLayout({ children }: { children: ReactNode }) {
  return (
    <>
      {children}
      <Analytics />
    </>
  )
}
```

## SolidStart

In the client entry:

```tsx title="src/entry-client.tsx"
import { mount, StartClient } from '@solidjs/start/client'
import { init } from '@invisitdev/web'

init({ site: '<site id>' })

mount(() => <StartClient />, document.getElementById('app')!)
```

## Vite

A Vite app (React, Vue, Svelte, Solid or none of them) starts in its entry module (`src/main.tsx`, `src/main.ts`), before the first render or `app.mount()`. React Router in framework mode is set up differently: see [React Router and Remix](#react-router-and-remix).

```ts title="src/main.ts"
import { init } from '@invisitdev/web'

init({ site: import.meta.env.VITE_INVISIT_SITE })
```

To serve the tracker from your own domain too, add the server side to whatever serves the built app; see the [server frameworks](https://invisit.dev/docs/server/frameworks.md).

## Angular

On Angular 19 and later, an app initializer next to the providers the CLI generated. With SSR, keep the `provideClientHydration(…)` the CLI added.

```ts title="src/app/app.config.ts"
import {
  ApplicationConfig,
  PLATFORM_ID,
  inject,
  provideAppInitializer,
  provideBrowserGlobalErrorListeners,
} from '@angular/core'
import { isPlatformBrowser } from '@angular/common'
import { provideRouter } from '@angular/router'
import { init } from '@invisitdev/web'
import { routes } from './app.routes'

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes),
    provideAppInitializer(() => {
      if (isPlatformBrowser(inject(PLATFORM_ID))) init({ site: '<site id>' })
    }),
  ],
}
```

## Gatsby, Docusaurus and VitePress

```ts title="gatsby-browser.tsx"
import type { GatsbyBrowser } from 'gatsby'
import { init } from '@invisitdev/web'

export const onClientEntry: GatsbyBrowser['onClientEntry'] = () => {
  init({ site: '<site id>' })
}
```

Docusaurus takes a client module, registered with `clientModules: ['./src/invisit.ts']` in `docusaurus.config.ts`:

```ts title="src/invisit.ts"
import { init } from '@invisitdev/web'

init({ site: '<site id>' })
```

VitePress runs the theme's `enhanceApp` during the build too, hence the `inBrowser` guard:

```ts title="docs/.vitepress/theme/index.ts"
import DefaultTheme from 'vitepress/theme'
import { inBrowser, type Theme } from 'vitepress'
import { init } from '@invisitdev/web'

export default {
  extends: DefaultTheme,
  enhanceApp() {
    if (inBrowser) init({ site: '<site id>' })
  },
} satisfies Theme
```

## WordPress

Print the script tag into every page's head from your child theme's `functions.php`, so a theme update never removes it:

```php title="wp-content/themes/my-child-theme/functions.php"
add_action('wp_head', function () {
    // Leave out your own visits while you are signed in to edit the site.
    if (current_user_can('edit_posts')) {
        return;
    }
    ?>
    <script async src="https://api.invisit.dev/invisit.js" data-site="<site id>" data-collector="https://api.invisit.dev"></script>
    <?php
});
```

Without a child theme, paste the same `<script>` tag into the site header with a code snippets plugin instead. Clicks become [events](https://invisit.dev/docs/guides/events.md#without-javascript) with `data-invisit-event` attributes, which the block editor takes in a Custom HTML block.

WordPress runs PHP, where `@invisitdev/server` does not. To serve the tracker from your own domain and record crawler reads, put the [Cloudflare Worker](https://invisit.dev/docs/server/frameworks.md#cloudflare-workers) in front of the site.

## Shopify

In Shopify admin, open **Online Store → Themes**, then **… → Edit code** on your live theme. Open `layout/theme.liquid` and paste the tag right before `</head>`. Wrap it in `request.design_mode` so your own visits in the theme editor are not counted:

```liquid title="layout/theme.liquid"
{% unless request.design_mode %}
  <script async src="https://api.invisit.dev/invisit.js" data-site="<site id>" data-collector="https://api.invisit.dev"></script>
{% endunless %}
```

Save, and every storefront page is counted from its next load. Checkout pages are served by Shopify and do not load theme code, so they stay unmeasured. The tag lives in the theme, so paste it again when you switch to another theme.

## Framer

Open **Project Settings → Custom Code** and choose **Add Script**. Paste the tag, place it at the **end of `<head>`** and set it to run **once**. Publish the site.

Page changes inside Framer are client-side navigation, and the tracker counts each one without a reload, so it only needs to run once.

## No framework

Use the [script tag](https://invisit.dev/docs/web/installation.md#script-tag). It is the same tracker.

1. Open the template every page shares, or each HTML file if there is none.
2. Paste the tag anywhere inside `<head>`. Last is fine: it loads async and never blocks the page.
3. Deploy. The next page load is counted.
