Frameworks · 2 min setup

How to add analytics to Remix and React Router v7

To add VisitTrack to Remix or React Router v7, render the script tag inside <head> in the Layout of app/root.tsx. It's server-rendered into every document, and every client-side navigation through <Link> or useNavigate() is counted as a pageview automatically.

Updated

Remix v2 and React Router v7's framework mode share the same model: app/root.tsx renders the whole HTML document, including <head>. A plain <script> element there is part of the server HTML, so the browser loads it once; React hydrates around it without running it again.

Remix at a glance

Where the tag goes
<head> in the Layout export of app/root.tsx
Route changes
Tracked automatically
Server-side events
From actions via POST /api/track
Works with
Remix v2, React Router v7 framework mode, SPA mode
Cookie banner
Not needed in cookieless mode

How to install VisitTrack on Remix

  1. Step 1: Copy your site id

    Copy the snippet from VisitTrack's install step (or Settings → General) and replace SITE_ID below.

  2. Step 2: Add the tag to app/root.tsx

    In React Router v7 the imports come from react-router; in Remix v2 they come from @remix-run/react. Everything else is identical.

    app/root.tsx
    import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
    
    export function Layout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <Meta />
            <Links />
            <script defer data-site="SITE_ID" src="https://visitrack.app/tracker.js" />
          </head>
          <body>
            {children}
            <ScrollRestoration />
            <Scripts />
          </body>
        </html>
      );
    }
    
    export default function App() {
      return <Outlet />;
    }
  3. Step 3: Allow it in your CSP, if you send one

    If you set a Content-Security-Policy in entry.server.tsx or at your host, add https://visitrack.app to script-src and connect-src.

  4. Step 4: Deploy and browse

    The dev server runs on localhost, which is ignored. Deploy and click between routes on the live site.

How to check VisitTrack is working on Remix

  1. View source on a deployed page — the tag is in the server HTML, inside <head>.
  2. Navigate between routes with <Link>s; the VisitTrack live view shows you and the Pages tab lists each route.
  3. DevTools → Network: one POST to visitrack.app/api/collect per navigation. Loader fetches and useFetcher calls don't change the URL, so they never add pageviews.

Remix and React Router navigate with history.pushState, which the tracker wraps (along with replaceState and popstate). <Link>, <Form> submissions that redirect, redirect() from an action and useNavigate() all land as pageviews once the URL changes.

Search-param-only updates — setSearchParams({ page: "2" }) — keep the same pathname and aren't counted again, which keeps filters and pagination from inflating pageviews.

How to track custom events and goals in Remix

The most reliable place to record a signup is the server, in the action that created the account. Pass the visitor id from the browser in a hidden field so the event joins the visitor's earlier pageviews and source.

app/routes/signup.tsx
// In the form (browser): <input type="hidden" name="vid" value={visitorId} />
// where visitorId = window.visitrack?.visitorId?.() read in a useEffect.

export async function action({ request }: Route.ActionArgs) {
  const form = await request.formData();
  const user = await createUser(String(form.get("email")));
  const visitorId = String(form.get("vid") || "");
  if (visitorId) {
    await fetch("https://visitrack.app/api/track", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.VISITRACK_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        type: "event",
        visitorId, // the _ana_vid value the browser sent you
        name: "signup",
        props: { method: "email" },
      }),
    });
  }
  return redirect("/welcome");
}

In the browser, window.visitrack?.("name", props) and data-vt-goal="…" work as in any React app. Server events need a secret key from Settings → API / MCP — never ship it to the client. Full reference: server-side events.

Revenue attribution on Remix

Start Stripe Checkout from an action and pass the same visitor id as client_reference_id; VisitTrack's webhook then credits the payment to the visitor's first referrer, campaign and landing page. Lemon Squeezy, Polar, Paddle and Razorpay each have their own metadata field — see Revenue attribution and Stripe.

Script options you might need on Remix

All optional — add them to the same tag. Full reference: script configuration.

AttributeWhat it does
data-cookielessStore nothing in the browser; the server derives a daily-rotating id. See cookieless mode.
data-exclude="/app/*"Never track these paths (comma-separated, * wildcard). Checked on every navigation.
data-allow-localTrack on localhost and *.local — for testing an install only.
data-debugLog every event the script sends, and why it skipped one, to the console.
data-auto="false"Turn off automatic events (downloads, contact clicks, form submits, rage clicks, JS errors, 404s).

Remix troubleshooting

Hydration warning mentioning the script

Make sure the tag is rendered identically on server and client — a hard-coded id, not one read from window. Reading the id from an env var in a loader is fine.

Pageviews doubled

The tag is in root.tsx and again in a nested route's head or a <script> from a component. Keep the single copy in Layout.

The server event returns 400

visitorId was empty or the body isn't valid JSON. Check the hidden field is filled before submit (it's set in an effect after the tracker loads).

Nothing in dev

Localhost is ignored by design. Add data-allow-local temporarily to test.

Remix analytics FAQ

Where do I add a script tag in Remix?

In app/root.tsx, inside the head rendered by the Layout export (or the default App component on older Remix versions). That file renders the document for every route, so the tag loads on every page exactly once.

Does VisitTrack track Remix client-side navigation?

Yes. Remix and React Router change the URL with the History API, and the tracker counts each pathname change as a pageview. No useLocation effect is needed.

Does this work with React Router v7 SPA mode (ssr: false)?

Yes. The root Layout is still rendered into the HTML shell at build time, so the tag is present on load, and navigation is tracked in the browser.

How do I track signups in Remix?

Send a signup event from the action that creates the account with POST /api/track, passing the visitor id from a hidden form field — or call window.visitrack("signup") in the browser after the action succeeds. Do one or the other, not both.

Can VisitTrack attribute Stripe revenue in a Remix app?

Yes. Pass the visitor id as client_reference_id when you create the Checkout Session, and connect the Stripe webhook in Settings → Revenue. See Stripe revenue attribution.

Keep going

Add VisitTrack to your Remix site

Cookie-free analytics with revenue attribution, live visitors, funnels and session replays. One script tag, every feature on every plan. 14 days free, no card required.

Not on Remix? See all 29 integration guides.