# Astro Installation Guide

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

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

This is the canonical Virent installation guide for Astro sites and applications.

### Install with your coding agent

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

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

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

Install two separate things:
1. Human Analytics (browser): Place the Virent script in the base Astro layout that wraps every page.
2. AI Crawler Analytics (server): Add src/middleware.ts in SSR/on-demand Astro deployments or install the same REST call at the edge for static hosting.

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:
- Add the browser script once in the shared Astro layout head.
- Use plain external script markup for the hosted Virent script; do not bundle it into an island.
- Keep event SDK code in client-side components only when custom events are needed.

AI Crawler Analytics best practices:
- Use Astro middleware only when the deployment has SSR or on-demand rendering.
- Keep the crawler secret in the server runtime and never ship it to client islands.
- Call next() so Astro can continue rendering the requested page.

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:
- Build and preview the Astro site.
- Inspect the page head and Network requests.
- Refresh Human Analytics status.
- Deploy with an SSR-capable adapter or edge layer.
- Request a page with curl -A "GPTBot".
- Refresh AI Analytics status.
```

## Research Notes

- Astro layouts are the preferred global HTML location for browser scripts.
- Astro middleware can intercept requests and responses for SSR or on-demand rendering.
- Fully static Astro output has no request-time middleware; AI Analytics must run at the hosting edge or reverse proxy in that case.

Primary references:

- https://docs.astro.build/en/guides/client-side-scripts/
- https://docs.astro.build/en/guides/middleware/

## Human Analytics

Add the Virent browser script to the shared layout that wraps all public pages.

```astro
---
// src/layouts/BaseLayout.astro
---
<html lang="en">
  <head>
	<script is:inline
      defer
      data-write-key="vha_pk_..."
      src="https://app.virent.app/js/script.js"
    ></script>
  </head>
  <body>
    <slot />
  </body>
</html>
```

Best practices:

- Use plain external script markup for the hosted Virent script.
- Do not wrap pageview tracking in a hydrated island.
- Confirm every public route uses the layout.
- Use client-side code only for optional custom event tracking.

Verification:

- Build and preview the Astro site.
- Inspect the rendered head and Network requests.
- Refresh Human Analytics status.

Troubleshooting:

- If the script is absent, confirm all routes use the edited layout.
- If CSP blocks the script, allow Virent in `script-src` and `connect-src`.
- If custom events do not fire, confirm event code runs in a client-side script or hydrated component.

### 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

For SSR or on-demand Astro deployments, use `src/middleware.ts`.

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

```ts
// src/middleware.ts
import type { MiddlewareHandler } from "astro"

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 const onRequest: MiddlewareHandler = async (context, next) => {
  const response = await next()
  const userAgent = context.request.headers.get("user-agent") ?? ""

  if (!staticAssetPattern.test(context.url.pathname) && aiCrawlerPattern.test(userAgent)) {
    await fetch(import.meta.env.VIRENT_INGEST_ENDPOINT, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "x-api-key": import.meta.env.VIRENT_INGEST_SECRET,
      },
      body: JSON.stringify({
        siteId: import.meta.env.VIRENT_SITE_ID,
        url: context.url.toString(),
        host: context.url.host,
        path: context.url.pathname,
        method: context.request.method,
        referer: context.request.headers.get("referer"),
        userAgent,
        statusCode: response.status,
        requestId: crypto.randomUUID(),
        timestamp: new Date().toISOString(),
      }),
    })
  }

  return response
}
```

Best practices:

- Use middleware only when the deployment has request-time rendering.
- Keep secrets in the server runtime, never in client islands.
- Await `next()` before sending telemetry so the final status code is available.

Verification:

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

- Confirm middleware runs in production.
- Refresh AI Analytics status.

Troubleshooting:

- If no crawler events arrive, verify the route is not fully prerendered static output.
- For static builds, move the same REST call to the hosting edge layer.
- If status codes are missing, ensure the middleware awaits `next()`.
