Matomo's snippet fires a pageview once, on initial script load - client-side route changes never trigger a full page load, so you must call setCustomUrl, then setDocumentTitle, then trackPageView inside your router's navigation event, every time the route changes.
Why this happens
Matomo, like classic Google Analytics, assumes a "page" corresponds to a real HTTP request. Single-page applications deliberately avoid full page loads on navigation, which is exactly what breaks that assumption - nothing happened, from Matomo's perspective, unless your code explicitly says a new virtual page occurred.
Skipping the setCustomUrl step is the most common partial-fix mistake: without it, every later trackPageView() call still reports the original URL the script first loaded on, making route-level analytics look like one long single-page session.
Fix it
- Import your router's location-change hook (for example useLocation() in React Router) at the top level of your app.
- On every location change, call
_paq.push(["setCustomUrl", window.location.pathname + window.location.search]); - Immediately after, call
_paq.push(["setDocumentTitle", document.title]); - Then call
_paq.push(["trackPageView"]); - Skip firing this on the very first render if your Matomo snippet already tracks the initial load - otherwise you double-count the landing page.
- Apply the same three-call pattern consistently for any virtual pageview you track (modals, tab switches), not only full route changes.
How to verify it worked
Navigate between two routes and check Matomo's Visits Log or Real-Time widget - each route change should appear as a distinct pageview with the correct URL path, not repeats of the original landing URL. In the Network tab, confirm each navigation produces a new request to matomo.php with an updated url parameter.