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.
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/netlifyor@astrojs/cloudflare) and the site's Server key from Site settings - AI crawler tracking (server).
Setup in 5 steps
-
01 Put the script in the base layout head
Open the layout that wraps every page and paste the Visitant line inside
<head>. Replaceyoursite.comwith your domain. Theis:inlinedirective tells Astro to write the tag into the HTML exactly as is, instead of trying to bundle it. Astro's directive reference saysis:inlineis implied whenever a script has any attribute other thansrc- ours hasdeferanddata-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.astrocomponent they all include. One line per page, never two. -
02 Build and deploy
Run
npm run buildand deploy as usual. Open the production domain and click through a few pages. The tracker ignoreslocalhoston purpose, sonpm run devshows nothing in the dashboard; adddata-localto the script if you want to test locally. -
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, useshistory.pushStatefor the new entry. The Visitant tracker wrapspushStatewhen 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-rerunto the Visitant script. That attribute forces a script to re-execute after every transition; the tracker would then wrappushStatea second time and count pages twice. Keep the plain line from step 1. -
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
middlewaremode, add the snippet below to your Express (or similar) server and replaceYOUR-KEYwith 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
standalonemode, or Vercel/Netlify with on-demand pages? Put the same check insrc/middleware.ts: exportonRequest(context, next), readcontext.request.headers.get('user-agent'), callconst res = await next(), and if the regex matches,fetchthe same JSON (ua,path,ip,status: res.status) with theX-Visitant-Keyheader before returningres. Remember it only runs for pages withprerender = false(oroutput: 'server'). -
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): thev.jsline 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 oneapi/erequest 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 missing | Astro 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 transitions | Either 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 development | By 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 site | Static 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 →