- âś… Basic SPA Matomo setup
- âś… Supports Next.js Pages Router (automatic tracking with
next/routerevents) - âś… Supports Next.js App Router (tracking with
usePathnameanduseSearchParams) - âś… Tracks route changes and page views
- âś… Tracks search queries on
/rechercheand/searchroutes - âś… Excludes URLs based on patterns
- âś… GDPR compliant (optional cookie-less tracking)
- âś… Custom event tracking
- âś… TypeScript support
Add the trackPagesRouter call in your _app.js:
import React, { useEffect } from "react";
import App from "next/app";
import { trackPagesRouter } from "@socialgouv/matomo-next";
const MATOMO_URL = process.env.NEXT_PUBLIC_MATOMO_URL;
const MATOMO_SITE_ID = process.env.NEXT_PUBLIC_MATOMO_SITE_ID;
function MyApp({ Component, pageProps }) {
useEffect(() => {
trackPagesRouter({ url: MATOMO_URL, siteId: MATOMO_SITE_ID });
}, []);
return <Component {...pageProps} />;
}
export default MyApp;Will track routes changes by default.
For Next.js App Router (Next.js 13+), create a client component to handle tracking. Use trackAppRouter and pass both pathname and searchParams to track the full URL including query parameters:
"use client";
import { trackAppRouter } from "@socialgouv/matomo-next";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
const MATOMO_URL = process.env.NEXT_PUBLIC_MATOMO_URL;
const MATOMO_SITE_ID = process.env.NEXT_PUBLIC_MATOMO_SITE_ID;
export function MatomoAnalytics() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams, // Pass URLSearchParams object directly
// Optional: Enable additional features
enableHeatmapSessionRecording: true,
enableHeartBeatTimer: true,
});
}, [pathname, searchParams]);
return null;
}Notes:
Add this component to your root layout wrapped in a Suspense boundary (required for useSearchParams):
// app/layout.js
import { Suspense } from "react";
import { MatomoAnalytics } from "./matomo";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Suspense fallback={null}>
<MatomoAnalytics />
</Suspense>
</body>
</html>
);
}Note: The Suspense boundary is required when using useSearchParams() in the App Router.
The App Router implementation includes the following features:
- Automatic route tracking: Detects changes in both pathname and search parameters
- Search tracking: Automatically uses
trackSiteSearchfor/rechercheand/searchroutes - Clean URLs: Tracks only the pathname without query strings (Matomo best practice)
- Referrer tracking: Properly tracks the previous page as referrer
- Custom callbacks: Supports
onRouteChangeStartandonRouteChangeCompletehooks
This wont track /login.php or any url containing ?token=.
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
excludeUrlsPatterns: [/^\/login.php/, /\?token=.+/],
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
excludeUrlsPatterns: [/^\/login.php/, /\?token=.+/],
});By default, the search tracking feature looks for a q parameter in the URL (e.g., /search?q=my+query). If your application uses a different parameter name for search queries, you can customize it:
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
searchKeyword: "query", // Will track searches from /search?query=my+search
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
searchKeyword: "query", // Will track searches from /search?query=my+search
});By default, search tracking is enabled for /recherche and /search routes. You can define custom routes that should be tracked as search pages:
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
searchRoutes: ["/find", "/discover", "/rechercher"], // Custom search routes
searchKeyword: "q", // Optional: customize the search parameter
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
searchRoutes: ["/find", "/discover", "/rechercher"], // Custom search routes
searchKeyword: "q", // Optional: customize the search parameter
});When a user visits any of the defined search routes, the library will automatically use trackSiteSearch instead of trackPageView.
By default, matomo-next tracks URLs with all their query parameters and hash fragments. You can enable URL cleaning to remove these parameters before tracking, which is useful for:
- Removing sensitive data (tokens, user IDs, etc.) from tracked URLs
- Normalizing URLs for better analytics aggregation
- Improving privacy compliance
Important: Search routes (/recherche, /search, or custom searchRoutes) automatically keep their query parameters even when cleanUrl is enabled, to preserve search tracking functionality with trackSiteSearch.
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
cleanUrl: true, // Remove query params and hash fragments from tracked URLs
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
cleanUrl: true, // Remove query params and hash fragments from tracked URLs
});Behavior examples:
// With cleanUrl: false (default)
// URL: /products?id=123&ref=home#section
// Tracked as: /products?id=123&ref=home#section
// With cleanUrl: true
// URL: /products?id=123&ref=home#section
// Tracked as: /products
// Search routes preserve query params even with cleanUrl: true
// URL: /search?q=keyword&category=docs&page=2
// Tracked as: /search?q=keyword&category=docs&page=2
// + trackSiteSearch("keyword")To disable cookies (for better GDPR compliance) set the disableCookies flag to true.
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
disableCookies: true,
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
disableCookies: true,
});Use the sendEvent helper for type-safe event tracking with auto-completion:
import { sendEvent } from "@socialgouv/matomo-next";
// Basic event with category and action
sendEvent({ category: "contact", action: "click phone" });
// Event with optional name parameter
sendEvent({
category: "video",
action: "play",
name: "intro-video",
});
// Event with optional name and value parameters
sendEvent({
category: "purchase",
action: "buy",
name: "product-123",
value: "99.99",
});For advanced use cases or custom tracking, use the push function directly:
import { push } from "@socialgouv/matomo-next";
// Track custom events
push(["trackEvent", "contact", "click phone"]);
// Track custom dimensions
push(["setCustomDimension", 1, "premium-user"]);
// Any other Matomo tracking method
push(["trackGoal", 1]);To enable Matomo's Heatmap & Session Recording feature:
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
enableHeatmapSessionRecording: true,
heatmapConfig: {
// Optional: capture keystrokes (default: false)
captureKeystrokes: false,
// Optional: capture only visible content (default: false, captures full page)
captureVisibleContentOnly: false,
},
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
enableHeatmapSessionRecording: true,
heatmapConfig: {
// Optional: capture keystrokes (default: false)
captureKeystrokes: false,
// Optional: capture only visible content (default: false, captures full page)
captureVisibleContentOnly: false,
},
});The Heatmap & Session Recording plugin will be automatically loaded and configured. It will:
- Load the
HeatmapSessionRecording/tracker.min.jsplugin - Configure keystroke capture and visible content settings
- Enable the recording after page load
To accurately measure time spent on pages, enable the HeartBeat Timer:
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
enableHeartBeatTimer: true,
heartBeatTimerInterval: 15, // Optional: interval in seconds (default: 15)
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
enableHeartBeatTimer: true,
heartBeatTimerInterval: 15, // Optional: interval in seconds (default: 15)
});The HeartBeat Timer sends periodic requests to Matomo to measure how long visitors stay on pages. This is particularly useful for tracking engagement on single-page applications.
Enable debug mode to see console logs for tracking events, excluded URLs, and Heatmap & Session Recording operations. This is useful for troubleshooting and development.
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
debug: true, // Enable debug logging
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
debug: true, // Enable debug logging
});When debug is enabled, you will see console logs for:
- Matomo initialization warnings
- Excluded URL tracking (when URLs match
excludeUrlsPatterns) - Heatmap & Session Recording plugin loading and configuration
- Any errors during script loading
Note: Debug mode should be disabled in production to avoid cluttering the console.
If you use a Content-Security-Policy header with a nonce attribute, you can pass it to the initialization function to allow the script to be executed.
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
nonce: "123456789",
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
nonce: "123456789",
});As the matomo-next injects a matomo script, if you use strict Trusted Types, you need to allow the script tag to be created by adding our policy name to your trusted types directive.
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types matomo-next;You can set a custom policy name by passing it to the initialization function.
Pages Router:
trackPagesRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
trustedPolicyName: "your-custom-policy-name",
});App Router:
trackAppRouter({
url: MATOMO_URL,
siteId: MATOMO_SITE_ID,
pathname,
searchParams,
trustedPolicyName: "your-custom-policy-name",
});The initialization functions have optional callback properties that allow for custom behavior to be added:
-
onRouteChangeStart(path: string) => void: This callback is triggered when the route is about to change. For Pages Router, it uses Next Router eventrouteChangeStart. For App Router, it's called when the pathname or searchParams change. It receives the new path as a parameter. -
onRouteChangeComplete(path: string) => void: This callback is triggered when the route change is complete. For Pages Router, it uses Next Router eventrouteChangeComplete. For App Router, it's called after the page view is tracked. It receives the new path as a parameter. -
onInitialization() => void: This callback is triggered when the function is first initialized. It does not receive any parameters. It could be useful to use it if you want to add parameter to Matomo when the page is render the first time. -
onScriptLoadingError() => void: This callback is triggered when the script does not load. It does not receive any parameters. useful to detect ad-blockers.
import React, { useEffect } from "react";
import { trackPagesRouter } from "@socialgouv/matomo-next";
function MyApp({ Component, pageProps }) {
useEffect(() => {
trackPagesRouter({
url: process.env.NEXT_PUBLIC_MATOMO_URL,
siteId: process.env.NEXT_PUBLIC_MATOMO_SITE_ID,
onRouteChangeStart: (path) => {
console.log("Route change started:", path);
// Your custom logic here
},
onRouteChangeComplete: (path) => {
console.log("Route change completed:", path);
// Your custom logic here
},
onInitialization: () => {
console.log("Matomo initialized");
},
onScriptLoadingError: () => {
console.error("Failed to load Matomo script");
},
});
}, []);
return <Component {...pageProps} />;
}
export default MyApp;"use client";
import { trackAppRouter } from "@socialgouv/matomo-next";
import { usePathname, useSearchParams } from "next/navigation";
import { useEffect } from "react";
export function MatomoAnalytics() {
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
trackAppRouter({
url: process.env.NEXT_PUBLIC_MATOMO_URL,
siteId: process.env.NEXT_PUBLIC_MATOMO_SITE_ID,
pathname,
searchParams, // Pass directly without .toString()
onRouteChangeStart: (path) => {
console.log("Route change started:", path);
// Your custom logic here
},
onRouteChangeComplete: (path) => {
console.log("Route change completed:", path);
// Your custom logic here
},
onInitialization: () => {
console.log("Matomo initialized");
},
onScriptLoadingError: () => {
console.error("Failed to load Matomo script");
},
});
}, [pathname, searchParams]);
return null;
}