Skip to content

Nuxt

Nuxt renders pages with Nitro on the server and hydrates on the client. Statable is a client-only script. Load it through Nuxt's head configuration so the tag ends up in the SSR HTML and the script runs once on hydration. Nuxt's router fires history.pushState on every navigation, which Statable picks up automatically.

This guide covers Nuxt 3 and Nuxt 4 (stable since July 2025). For Nuxt 2, use the legacy head option in nuxt.config.js with the same script attributes.

Install

Approach 1: nuxt.config.ts

The simplest option. Register the script globally in app.head.script:

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: `https://statable.com/js/${process.env.NUXT_PUBLIC_STATABLE_ID}/s.js`,
          defer: true,
        },
      ],
    },
  },
  runtimeConfig: {
    public: {
      statableId: process.env.NUXT_PUBLIC_STATABLE_ID,
    },
  },
})

Set the env var in .env:

NUXT_PUBLIC_STATABLE_ID=your_site_id

Nuxt renders the tag inline in <head> during SSR, so the snippet ships with the first byte. defer keeps it from blocking parse.

Approach 2: useHead in app.vue

If you prefer to keep tracking config close to your app entry, use the auto-imported useHead composable:

<!-- app.vue -->
<script setup lang="ts">
const config = useRuntimeConfig()

useHead({
  script: [
    {
      src: `https://statable.com/js/${config.public.statableId}/s.js`,
      defer: true,
    },
  ],
})
</script>

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

useHead is reactive and SSR-aware. The tag renders on the server and Unhead reconciles it on the client without re-injecting.

Approach 3: Nuxt Scripts (useScript)

Nuxt Scripts (v1 stable) is the modern way to load third-party tags in Nuxt. It defers execution to onNuxtReady by default, so the script never blocks hydration and never appears in the SSR response. Good when Core Web Vitals matter more than capturing the very first paint.

Install the module:

npx nuxi module add scripts

Then call useScript in app.vue or any layout:

<!-- app.vue -->
<script setup lang="ts">
const config = useRuntimeConfig()

useScript({
  src: `https://statable.com/js/${config.public.statableId}/s.js`,
  defer: true,
}, {
  trigger: 'onNuxtReady',
})
</script>

Trade-off: with onNuxtReady the script loads after Nuxt finishes hydrating, so the very first pageview fires a fraction of a second later than with Approach 1 or 2. For most sites that's invisible. If you want the script live on the SSR response, stay with Approach 1.

Tracking custom events

Wrap window.statable in a typed composable so every component gets autocomplete and SSR-safe calls:

// composables/useStatable.ts
export function useStatable() {
  function track(event: string, props?: Record<string, unknown>) {
    if (import.meta.server) return
    window.statable?.t?.(event, props)
  }

  return { track }
}

declare global {
  interface Window {
    statable: {
      t: (event: string, props?: Record<string, unknown>) => void
    }
  }
}

Use it in any component:

<script setup lang="ts">
const { track } = useStatable()

function onSubmit() {
  track('Newsletter Signup', { source: 'footer' })
}
</script>

Tracking page views in SPA

<NuxtLink> and programmatic navigateTo both call history.pushState. Statable listens for that and counts the pageview. No router hook required.

Excluding internal traffic

Statable respects a per-browser opt-out flag in localStorage. To exclude your own dev visits, run this once in DevTools on the live site:

localStorage.setItem('analytics_ignore', 'true')

Remove the flag with localStorage.removeItem('analytics_ignore'). Full procedure in Verify installation.

Verify it's working

  • Open Statable Realtime in your dashboard.
  • Navigate between pages on your Nuxt site. Each view should appear within seconds.
  • See Verify installation for the full checklist.

Common pitfalls

  • Calling window.statable from setup() without an SSR guard. The first render runs on the server, where window is undefined. Wrap calls in import.meta.client or use the composable above.
  • Hardcoding the Site ID. Always read it from runtimeConfig.public. Then you can swap IDs between staging and production without rebuilding.
  • Loading the script in body instead of head. Statable needs to register early to capture engagement and scroll. Use app.head.script, useHead, or useScript.
  • Mixing Approach 1 and Approach 2. Both render the same tag. Pick one. Two of them ship the script twice.
  • ssr: false (SPA mode). No SSR HTML is sent, so all three approaches load the script on the client only. Behavior is the same, the first pageview just waits for the JS bundle.

See also: Install the tracking script, Custom events, JavaScript API.


Ready to take control of your web analytics? Try Statable free for 30 days — no credit card required, full feature access, GDPR-compliant by default. Start your free trial or view a live demo.