Clycyo
Frameworks5 min read

Remix Analytics: Setup for Nested Routes and Transitions

Add privacy-first analytics to Remix: root.tsx installation, client-side transition tracking, and event calls that survive progressive enhancement.

Remix's progressive-enhancement ethos creates a specific analytics requirement most guides miss: your forms work without JavaScript, so your measurement must too — at least for the moments that matter. The setup below handles the standard SPA tracking and the no-JS fallback path.

Installation in root.tsx

// app/root.tsx
export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
        <script
          defer
          src="https://clycyo.com/tracker.js"
          data-tracking-id="YOUR_TRACKING_ID"
        />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Client-side transitions (Link navigations, navigate()) flow through the History API and are recorded automatically as pageviews with transition timing. Nested route changes that alter the URL count; loader revalidations that do not change the URL correctly do not.

Events: enhanced and unenhanced paths

With JS available, track from the component as usual:

<Form method="post" onSubmit={() => {
  window.webanalytics?.track('contact_submitted');
}}>

For the no-JS path — Remix's specialty — record the conversion server-side in the action, posting to the collect API with a visitor ID you stored in the session when the user first identified (or skip attribution for anonymous no-JS forms; counting beats nothing):

// In your action, after a successful mutation
await fetch('https://clycyo.com/api/collect', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    tracking_id: process.env.CLYCYO_TRACKING_ID,
    type: 'event',
    visitor_id: session.get('clycyo_visitor_id'),
    event_name: 'contact_submitted',
  }),
});

Identity

// Client, after login
window.webanalytics?.identify(user.email);
// Then persist getVisitorId() via a fetcher to your session/user record

Stored once, the visitor ID powers both no-JS action events above and billing-webhook revenue later — one join key for everything.

Automatic coverage

  • Pageviews with load and transition timing; field Web Vitals.
  • JS errors per route — including ErrorBoundary-swallowed ones that pageview data alone hides.
  • Clicks, referrers, first-touch UTM persistence for attribution.

One tag, one optional server-side pattern, and your Remix app measures both of its personalities. Full payload reference in the docs.