Docs

04 · Docs

Astro analyticswith juuna.

One script tag in the base layout covers a standard Astro site whole; view transitions need one listener. Everything here is copy-paste correct for this instance.

Plain markdown for AI agents · /docs/astro/llms.txt

llms.txt

Astro analytics with juuna

Last updated · 2026-08-31

TL;DR: to add analytics to an Astro site, put one script tag in the base layout's head. Every navigation in a standard Astro site is a full page load, so the tag records every pageview with no extra code; only view transitions need one listener.

juuna is product analytics on a dedicated instance of your own: traffic, funnels, heatmaps, session replay and Web Vitals from one small, dependency-free SDK. Every event belongs to a source identified by a public write key (wk_…), created in the dashboard under Settings → Sources; use yours in place of wk_YOUR_WRITE_KEY below. The full integration guide covers the SDK reference, autocapture and the HTTP API, and this page is served as plain markdown at https://juuna.app/docs/astro/llms.txt for AI agents.

Add the script tag

---
// src/layouts/Base.astro
---
<html lang="en">
  <head>
    <script
      is:inline
      defer
      src="https://juuna.app/sdk/juuna.js"
      data-write-key="wk_YOUR_WRITE_KEY"
      data-api-host="https://juuna.app"></script>
  </head>
  <body><slot /></body>
</html>

is:inline tells Astro to leave the tag exactly as written instead of processing it. The tag initialises itself from its data-* attributes, records the pageview, and exposes window.juuna for custom events. A standard Astro site is a multi-page site: each navigation loads a new document, the tag runs again, and every page is counted, so there is no route-change code to write.

Optional autocapture rides on three more attributes: data-web-vitals="true", data-click-tracking="true" and data-scroll-tracking="true".

View transitions

With Astro's <ClientRouter /> (view transitions), navigations swap the DOM instead of loading a new document and scripts do not run again, so only the first page would be counted. Add one listener beside the tag:

<script is:inline>
  document.addEventListener('astro:after-swap', function () {
    if (window.juuna) window.juuna.page()
  })
</script>

astro:after-swap fires after each client-side swap and never on the initial load, so each view is recorded exactly once: the tag counts the first document, the listener counts every swap after it.

The npm alternative

For custom events from bundled code, or for session replay (which the script build excludes), use the package:

npm install @muziris/juuna
<script>
  import { juuna } from '@muziris/juuna'

  juuna.init('wk_YOUR_WRITE_KEY', { apiHost: 'https://juuna.app' })
  juuna.page()
  document.addEventListener('astro:after-swap', () => juuna.page())
</script>

Astro bundles this module script and the browser runs it once per document, so a standard site records one pageview per load; with <ClientRouter /> it runs once per visit and the listener covers the swaps. Do not combine it with the script tag, or the first pageview counts twice.

Custom events and identity

The same client that records pageviews takes named events and identity. Call it from the package import, or from window.juuna when using the script tag:

juuna.track('Signup Completed', { plan: 'pro' })
juuna.identify('user_42', { email: 'ada@example.com' }) // after sign-in
juuna.reset()                                           // on logout

Pageviews, sessions, visitors, referrers, devices, browsers and geography are all recorded without any of this. Add a custom event only where a real product question needs one; identify stitches a visitor's anonymous history to their user id, including activity from before they signed in.

Verify it works

Click through a few pages in a normal browser window. Automated browsers are deliberately ignored (the SDK records nothing when navigator.webdriver is set), so a Playwright or Selenium session will never show up. Then check either end of the wire:

  • Network tab: filter for /api/v1/batch. Events batch in memory and flush every 5 seconds by default, at 20 queued events, or when the tab is hidden, so allow a few seconds. A delivered batch answers {"accepted":1,"rejected":0} with a 200.
  • Dashboard: the source's overview shows live visitors and new events within seconds of a flush.

A 401 means the write key is wrong or unknown. A 403 means the source has allowed domains configured and the origin you are testing from is not among them. If nothing is sent at all, check that localStorage.juuna_ignore is not 'true' in that browser: it is the per-browser opt-out.

FAQ

Do pageviews double count with view transitions?

Not with the pattern on this page. The tag records the first document load, and astro:after-swap fires only on client-side swaps, never on the initial load, so each view is counted exactly once.

Does the tag slow the site down?

The tag is 1.0 KB gzipped, loads with defer, has no dependencies, and never throws into the host page. Autocapture, when switched on, arrives as a second 1.9 KB file loaded on demand.

Can an Astro site use session replay?

Yes, through the npm package with sessionReplay: true; the recorder, rrweb, lazy-loads as its own chunk and recordings are masked by default. The hosted script tag build excludes replay.