# Nuxt Installation Guide

> Install Virent Human Analytics and AI Crawler Analytics on Nuxt.

Canonical: https://virent.app/docs/installation/nuxt
Last updated: 2026-08-26

This is the canonical Virent installation guide for Nuxt applications.

### Install with your coding agent

Paste this prompt into Claude Code, Codex, Cursor or another coding agent.

```text
Install Virent analytics on my Nuxt website.

Read the canonical guide first and follow its code exactly:
https://virent.app/docs/installation/nuxt.md

Install two separate things:
1. Human Analytics (browser): Add the Virent browser script through useHead in the root Nuxt app component.
2. AI Crawler Analytics (server): Set server env values and add a Nitro server plugin for completed crawler requests.

Values I will give you (they are in Virent under Settings > Connection):
- Publishable browser key, starts with vha_pk_. Safe in browser code.
- Site ID (VIRENT_SITE_ID) and server secret (VIRENT_INGEST_SECRET, starts with vha_sk_). Server only.
Ask me for them. Never invent a key or use a masked preview such as vha_sk_••••.

Human Analytics best practices:
- Use useHead in app/app.vue for one global script install.
- Set defer on the script so the Nuxt app is not blocked.
- Keep custom event SDK usage in client-only composables or plugins.

AI Crawler Analytics best practices:
- Use a Nitro server plugin and its afterResponse hook so crawler telemetry includes the final response status.
- Use runtime config or platform environment variables for the secret; never expose it through public runtime config.
- Filter out static assets and API routes unless those routes should be measured.

Rules:
- Keep VIRENT_INGEST_SECRET in server environment variables. Never put it in client code, public env variables (NEXT_PUBLIC_, VITE_, PUBLIC_) or committed files.
- Exclude API routes, static assets, images, favicons, robots.txt and sitemaps from crawler capture.
- If the site sends a Content-Security-Policy, allow https://app.virent.app in script-src and connect-src.
- Do not change unrelated code.

When you finish, list the files you changed and how I verify each path:
- Open a Nuxt page and inspect the head for the Virent script.
- Navigate to another route and confirm pageview traffic.
- Refresh Human Analytics installation status.
- Start Nuxt in a server-capable mode.
- Request a route with curl -A "GPTBot".
- Refresh AI Analytics status in Virent.
```

## Research Notes

- Nuxt `useHead` manages scripts in the document head.
- Nitro server plugins can subscribe to `afterResponse`, after the final status is known.
- Static-only output has no request-time Nitro hook; install crawler tracking at the hosting provider instead.

Primary references:

- https://nuxt.com/docs/4.x/api/composables/use-head
- https://nuxt.com/docs/4.x/directory-structure/server

## Human Analytics

Add the browser script globally in `app/app.vue`.

```vue
<script setup lang="ts">
useHead({
  script: [
    {
      src: "https://app.virent.app/js/script.js",
      defer: true,
      "data-write-key": "vha_pk_...",
    },
  ],
})
</script>

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>
```

Best practices:

- Keep the script global in `app/app.vue`.
- Use `defer` so Nuxt hydration is not blocked.
- Keep custom event code in client-side composables or plugins.
- Use public runtime config only for publishable values.

Verification:

- Open a Nuxt route and inspect the head.
- Navigate to another route and confirm pageview traffic.
- Refresh Human Analytics status.

Troubleshooting:

- If the script is absent, confirm the project uses `app/app.vue` and the `useHead` call is not conditional.
- If CSP blocks Virent, allow the origin in `script-src` and `connect-src`.
- If local events are expected, temporarily add `data-allow-localhost="true"`.

### First-party (recommended)

Ad blockers and privacy tools often block third-party analytics hosts, which silently drops visits. Relay the script and events through your own domain under `/_vr`, and load the script with `data-api="/_vr"` so events go to your domain too. The relay must forward the visitor's IP and country in `x-virent-forwarded-for` and `x-virent-forwarded-country`, with your website's secret key (`vha_sk_…`) in `x-virent-proxy-key`; without the key Virent ignores the forwarded headers and sees your server's network instead of your visitors'. Relay only `/_vr/js/script.js` and `/_vr/v1/*`, and never forward cookies or authorization headers. See [First-party analytics](/docs/installation/first-party) for how Virent trusts these headers.

With nginx, add these locations to the site's server block:

```text
location = /_vr/js/script.js {
  proxy_pass https://app.virent.app/js/script.js;
  proxy_set_header Host app.virent.app;
  proxy_ssl_server_name on;
  proxy_set_header Cookie "";
}

location /_vr/v1/ {
  proxy_pass https://app.virent.app/v1/;
  proxy_set_header Host app.virent.app;
  proxy_ssl_server_name on;
  proxy_set_header Cookie "";
  proxy_set_header Authorization "";
  proxy_set_header X-Virent-Forwarded-For $remote_addr;
  # Your website's secret key: Virent trusts the forwarded IP only with it.
  proxy_set_header X-Virent-Proxy-Key "vha_sk_...";
}
```

On Cloudflare, deploy a Worker on the `example.com/_vr/*` route that copies only `accept-language`, `content-type`, `origin` and `user-agent`, sets `x-virent-forwarded-for` from `cf-connecting-ip`, `x-virent-forwarded-country` from `request.cf.country` and `x-virent-proxy-key` from a Worker secret holding your website's secret key, and fetches the same path without `/_vr` from `https://app.virent.app`. The dashboard install panel has the full Worker.

Then load the script from your domain:

```html
<script
  defer
  data-api="/_vr"
  data-write-key="vha_pk_..."
  src="/_vr/js/script.js"
></script>
```

## AI Analytics

Add private env values:

```env
VIRENT_SITE_ID=site_...
VIRENT_INGEST_SECRET=vha_sk_...
VIRENT_INGEST_ENDPOINT=https://app.virent.app/v1/ingest/bot
```

Create a Nitro server plugin:

```ts
// server/plugins/virent-crawler.ts
const staticAssetPattern = /\.(?:png|jpg|jpeg|gif|svg|ico|webp|css|js|map|txt|xml)$/i
const aiCrawlerPattern = /GPTBot|OAI-SearchBot|ChatGPT-User|ClaudeBot|claudebot|Claude-Web|Claude-User|Claude-SearchBot|PerplexityBot|PerplexityBot\/|Perplexity-User|Perplexity-User\/|Google-CloudVertexBot|DeepSeekBot|DeepSeekSpider|CCBot|Bytespider|meta-externalagent|meta-webindexer|meta-externalfetcher|Amazonbot|DuckAssistBot|YouBot/i

export default defineNitroPlugin((nitroApp) => {
	nitroApp.hooks.hook("afterResponse", async (event) => {
	  const requestUrl = getRequestURL(event)
	  const userAgent = getHeader(event, "user-agent") ?? ""

	  if (staticAssetPattern.test(requestUrl.pathname) || !aiCrawlerPattern.test(userAgent)) {
		return
	  }

	  await $fetch(process.env.VIRENT_INGEST_ENDPOINT!, {
		method: "POST",
		headers: { "x-api-key": process.env.VIRENT_INGEST_SECRET! },
		body: {
		  siteId: process.env.VIRENT_SITE_ID!,
		  url: requestUrl.toString(),
		  host: requestUrl.host,
		  path: requestUrl.pathname,
		  method: event.method,
		  referer: getHeader(event, "referer") ?? null,
		  userAgent,
		  statusCode: event.node.res.statusCode,
		  requestId: crypto.randomUUID(),
		  timestamp: new Date().toISOString(),
		},
	  })
	})
})
```

Best practices:

- Use the Nitro `afterResponse` hook so telemetry has the final response status and does not delay route handling.
- Use private runtime config or server environment variables for the secret.
- Move tracking to the CDN/edge layer for static-only Nuxt output.

Verification:

```sh
curl -A "GPTBot" https://your-domain.com/
```

- Confirm the Nitro server plugin runs.
- Refresh AI Analytics status.

Troubleshooting:

- If `afterResponse` does not run, confirm the deployment uses a server/Nitro runtime.
- If the secret is undefined, check private runtime config and deployment env names.
- If events are duplicated, ensure only one middleware or edge layer sends events.
