Clycyo
Frameworks5 min read

Nuxt Analytics: SSR, Hydration, and Route Tracking Done Right

The Nuxt analytics setup that works across SSR and client navigation, with app.head configuration and events from composables.

Nuxt complicates analytics in exactly one way: your code runs on the server first. A tracker referenced carelessly inside setup() throws during SSR ('window is not defined'), and a script added per-page double-loads. Put the tag in the right place once and both problems vanish — here is that place, plus the Nuxt-specific patterns for events and identity.

Installation via nuxt.config

// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [
        {
          src: 'https://clycyo.com/tracker.js',
          defer: true,
          'data-tracking-id': 'YOUR_TRACKING_ID',
        },
      ],
    },
  },
});

This renders the tag into the initial HTML — loaded once, before hydration, never duplicated by client navigation. Both rendering modes work: with SSR, the first pageview fires on load with full navigation timing; with ssr: false, it behaves like any SPA. Subsequent vue-router navigations are captured automatically through History API instrumentation — no router hooks, no plugin.

Events: client-side only, by construction

// composables/useAnalytics.ts
export function useAnalytics() {
  const track = (name: string, props?: Record<string, unknown>) => {
    if (import.meta.client) {
      window.webanalytics?.track(name, props);
    }
  };
  return { track };
}

The import.meta.client guard makes the composable safe to call from anywhere — components, plugins, middleware — without SSR crashes. Use it for the events that matter (signup, activation, plan clicks) and let automatic capture handle the rest.

Identity and revenue

// After login (client side)
window.webanalytics?.identify(user.email);
await $fetch('/api/me', {
  method: 'PATCH',
  body: { clycyo_visitor_id: window.webanalytics?.getVisitorId() },
});

Persisting the visitor ID lets your Nitro server routes post revenue events from billing webhooks — server-to-server, blocker-proof, joined to the visitor's first-touch UTM.

Nuxt-specific notes

  • useHead per-page is unnecessary for the tracker — app.head loads it globally. Reserve per-page head work for metadata.
  • Hybrid rendering (routeRules) is fine: prerendered, ISR, and SSR pages all carry the same tag.
  • Watch INP, not just LCP: Nuxt's hydration cost shows up in interaction latency; the tracker reports field INP per real visitor, which Lighthouse cannot.

One config block, one composable, two identity lines — that is the entire integration. Snippets for every framework live in the quickstart.