Muro
Advanced

Single-page apps

How Muro tracks client-side navigation in React, Next.js, Vue, Astro and other single-page apps, and what it ignores on purpose.

Muro tracks navigation in single-page apps on its own. You install the same one-line script as on any other site, and each route change becomes a pageview. This page explains what counts as a navigation, what doesn't, and how to send custom events from an SPA.

What counts as a pageview

Muro sends a pageview:

  • When the script first runs on a full page load.
  • Each time your app calls history.pushState. This is what Next.js <Link> and router.push(), React Router, Vue Router in history mode, Nuxt, SvelteKit and Astro's view transitions do when they change the route.
  • When the visitor goes back or forward (the popstate event).

Each pageview records the path and query string at that moment. You don't need to subscribe to router events or call anything yourself.

Don't track route changes yourself

Adding a router hook that calls window.muro.track on every route change, or a useEffect on the location, sends a second event for every page. Let the script handle navigation.

Why replaceState is ignored

Muro deliberately doesn't listen to history.replaceState. Frameworks such as Next.js, Nuxt and Remix call it during hydration on almost every page load, and many apps call it to update query parameters (for filters or search boxes) without a real navigation.

If Muro counted it, every first page would be counted twice and your bounce rate would drop to near zero, because almost no visit would look like a one-page visit. Ignoring it keeps one pageview per real navigation.

The trade-off: if your app changes routes with replaceState only, those changes aren't counted as new pages.

Hash routing isn't tracked

Muro records the path and query string, never the # fragment. With hash-based routing, URLs like /#/pricing and /#/settings all have the path /, so every route counts as the same page.

Switch your router to history mode to get per-page numbers:

  • React Router: use BrowserRouter or createBrowserRouter, not HashRouter.
  • Vue Router: use createWebHistory(), not createWebHashHistory().

In-page anchor links (#pricing on a long landing page) don't create separate pages either. Some browsers fire popstate when the hash changes, which can add a pageview for the same path.

Custom events in SPAs

Both ways of sending custom events work across route changes:

  • data-muro-event on a button or link. Muro listens once on the whole document, so elements rendered later, on any route, are covered.
  • window.muro?.track(name, props) from your code. Call it in an event handler or after an action succeeds, never during render or on the server. Use ?. because window.muro only exists once the async script has loaded.

If a success page is a client-side route, you may not need code at all: mark that path as a goal and Muro counts visits to it.

In React, an event fired from a useEffect runs twice in development under StrictMode. Guard it with a ref if you fire events on mount. See Track custom events.

Verify it works

  1. Open your app, then open DevTools and go to the Network tab, filtered by collect.
  2. Reload. You should see one POST to cdn.muroanalytics.com/collect with status 202.
  3. Click a link to another route. Exactly one new collect request appears.
  4. Press back. One more appears.

If you see two requests per navigation, see Duplicate pageviews.

Next steps

On this page