There are no wrapper packages: call init() from @invisitdev/web 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 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.
Next.js
A client component rendered inside <body> in the root layout (App Router):
'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
}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:
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/.
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.
<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.
<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_*.
'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
}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:
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.
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.
Angular
On Angular 19 and later, an app initializer next to the providers the CLI generated. With SSR, keep the provideClientHydration(…) the CLI added.
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
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:
import { init } from '@invisitdev/web'
init({ site: '<site id>' })VitePress runs the theme’s enhanceApp during the build too, hence the inBrowser guard:
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 ThemeWordPress
Print the script tag into every page’s head from your child theme’s functions.php, so a theme update never removes it:
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 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 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:
{% 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. It is the same tracker.
- Open the template every page shares, or each HTML file if there is none.
- Paste the tag anywhere inside
<head>. Last is fine: it loads async and never blocks the page. - Deploy. The next page load is counted.