ENPL Log in Start free
// Integrations / Astro

Visitant on Astro

Astro sites are mostly static HTML, which is the easiest case: one script line in the base layout and every page is counted. If you use view transitions or on-demand rendering, two short notes below cover exactly what changes.

Time: 10 minCode: 1 componentCookies: 0

What you need

  • An Astro project with a shared layout (usually src/layouts/Layout.astro) that every page uses.
  • A free Visitant account with your domain added, for example yoursite.com.
  • For the AI crawler part only: on-demand rendering with an adapter (@astrojs/node, @astrojs/vercel, @astrojs/netlify or @astrojs/cloudflare) and the site's Server key from Site settings - AI crawler tracking (server).

Setup in 5 steps

  1. 01 Put the script in the base layout head

    Open the layout that wraps every page and paste the Visitant line inside <head>. Replace yoursite.com with your domain. The is:inline directive tells Astro to write the tag into the HTML exactly as is, instead of trying to bundle it. Astro's directive reference says is:inline is implied whenever a script has any attribute other than src - ours has defer and data-domain - but writing it out makes the intent clear to the next person.

    ---
    // src/layouts/Layout.astro
    const { title } = Astro.props;
    ---
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <title>{title}</title>
        <script is:inline defer data-domain="yoursite.com" src="https://annuora.com/v.js"></script>
      </head>
      <body>
        <slot />
      </body>
    </html>

    Several layouts (blog, docs, landing)? Paste the line in each, or in a small Head.astro component they all include. One line per page, never two.

  2. 02 Build and deploy

    Run npm run build and deploy as usual. Open the production domain and click through a few pages. The tracker ignores localhost on purpose, so npm run dev shows nothing in the dashboard; add data-local to the script if you want to test locally.

  3. 03 Using view transitions? Nothing extra - and one thing to avoid

    With <ClientRouter /> in the head, Astro navigates without a full reload and, per the view transitions guide, uses history.pushState for the new entry. The Visitant tracker wraps pushState when it first loads, so each navigation is recorded as a pageview automatically. During the head swap Astro keeps scripts that also exist on the new page, so the tracker is neither removed nor re-run.

    Do not add data-astro-rerun to the Visitant script. That attribute forces a script to re-execute after every transition; the tracker would then wrap pushState a second time and count pages twice. Keep the plain line from step 1.

  4. 04 Count AI crawlers on the server (SSR only)

    GPTBot, ClaudeBot and the other 44 crawlers Visitant recognises never run JavaScript, so the script cannot see them. On a purely static build there is no server code at request time - Astro's middleware guide states middleware runs at build time for prerendered pages - so a static site on Netlify or Vercel cannot count crawlers unless you put the Cloudflare Workers snippet from the dashboard in front of it. If your site uses on-demand rendering with the Node adapter in middleware mode, add the snippet below to your Express (or similar) server and replace YOUR-KEY with your Server key. Only requests whose user agent matches the crawler list leave the server, so human traffic costs nothing.

    const AI_UA = /bot|crawl|spider|GPTBot|ChatGPT-User|OAI-SearchBot|Claude|anthropic|Perplexity|Google-|GoogleOther|Applebot|meta-external|Meta-WebIndexer|Amazonbot|Amzn-|Bytespider|CCBot|MistralAI|DuckAssist|YouBot|xAI|Grok|cohere|Diffbot|Kimi|Qwen|Manus|Firecrawl/i;
    app.use((req, res, next) => {
      const ua = req.get('user-agent') || '';
      if (AI_UA.test(ua)) res.on('finish', () => fetch('https://annuora.com/api/bot', {
        method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Visitant-Key': 'YOUR-KEY' },
        body: JSON.stringify({ ua, path: req.originalUrl, ip: req.ip, status: res.statusCode })
      }).catch(() => {}));
      next();
    });

    Node adapter in standalone mode, or Vercel/Netlify with on-demand pages? Put the same check in src/middleware.ts: export onRequest(context, next), read context.request.headers.get('user-agent'), call const res = await next(), and if the regex matches, fetch the same JSON (ua, path, ip, status: res.status) with the X-Visitant-Key header before returning res. Remember it only runs for pages with prerender = false (or output: 'server').

  5. 05 Confirm the crawler channel

    Deploy again and open Site settings - AI crawler tracking (server) in the dashboard: the box says Waiting for the first crawler hit until a crawler comes by, then Working: N crawler hits recorded. To test immediately: curl -A "GPTBot/1.4" https://yoursite.com/. Crawler hits never count toward your plan's event limit.

Is it working? Check in 60 seconds

Do these on the deployed site, with the Visitant dashboard open in a second tab.

  • The current visitors counter at the top of the dashboard shows 1 while your site is open.
  • View the page source (Ctrl+U): the v.js line is inside <head>, untouched by the bundler (no hashed filename), exactly once.
  • With <ClientRouter />: click two internal links without a reload; both paths appear in the Pages table, one pageview each, and Network in developer tools shows one api/e request per navigation.
  • SSR with the snippet: run curl -A "GPTBot/1.4" https://yoursite.com/; within a minute AI crawlers reading your site lists GPTBot with 1 hit.
  • Open the site from a Perplexity answer (or append ?utm_source=perplexity.ai): the Sources table shows Perplexity in the AI channel.

Common problems

The script tag in the built HTML looks different or is missingAstro tried to process it as a bundled script. Make sure the line has is:inline and is written directly in the .astro layout, not imported from a .ts file.
Pages are counted twice with view transitionsEither data-astro-rerun is on the script, or two layouts both include it and a page uses one inside the other. Remove the attribute and keep one line per page.
Nothing shows up in developmentBy design: the tracker does not run on localhost. Add data-local to the script while testing and remove it afterwards.
Crawler hits stay at zero on a static siteStatic pages have no server code at request time, so nothing can report the crawler. Either switch the relevant routes to on-demand rendering, or use the Cloudflare Workers snippet in front of the site.

Questions

Does Visitant need a cookie banner on my Astro site?

No. The script sets no cookies, keeps no identifiers in local storage and Visitant never stores IP addresses. If you have a banner for other tools, do not gate the script behind it.

Will it slow down my Lighthouse score?

In our experience by nothing measurable: under 2 KB gzipped, loaded with defer, one small request per page. Astro's own islands script is larger.

Can I fire custom events from an Astro component?

Yes. After the script loads, window.visitant("Signup", { props: { plan: "pro" } }) works from any client-side script, and elements with data-v-event="Name" report a click with no code at all.

Create a free account - the dashboard shows these steps with your own domain and key filled in.

Start free →