Skip to main content

Client-side tracking

The client-side tracker sees what the server cannot: how long someone stayed, what they clicked, their screen and browser, and the page-performance timings Matomo's own report is built from. One Blade directive renders it.

<head>
{{-- ... --}}
@matomoScript
</head>

That is the whole integration. The directive renders an empty string unless the package is enabled, js.enabled is true and the instance is configured — so it is safe to drop into a layout that also serves local development and CI, with no @if around it.

What it emits

By default: a dns-prefetch link for the Matomo origin, the _paq bootstrap, the asynchronous loader for matomo.js, and a <noscript> fallback pixel so a visitor without JavaScript still produces a page view.

The order of the _paq commands is deliberate, because Matomo applies some of them only if they arrive before the page view:

  1. disableCookies — when privacy.cookieless is true (the default).
  2. The consent call for your privacy.consent posture, if any.
  3. setDoNotTrack — when privacy.honor_dnt is true (the default).
  4. Any statically configured custom dimensions.
  5. disablePerformanceTracking — only when js.performance is false.
  6. trackPageView.
  7. Content-impression scanning, if content tracking is on.
  8. enableLinkTracking, then the heartbeat timer.
  9. The tracker URL and site id, then the loader.
  10. The soft-navigation listeners, if spa.enabled.

Every value the package embeds in that script is JSON-encoded as a JavaScript string literal, or HTML-escaped when it lands in an attribute.

The settings

'js' => [
'enabled' => env('MATOMO_JS_ENABLED', true),
'host' => env('MATOMO_JS_HOST'), // separate asset host, e.g. a Matomo Cloud CDN
'tag_manager' => null, // full MTM container URL
'enable_link_tracking' => true,
'performance' => true,
'custom_dimensions' => [],
'content_tracking' => false,
'heartbeat' => 15, // enableHeartBeatTimer seconds; 0 to disable
'noscript' => true,
'dns_prefetch' => true,
],

enabled turns the client tracker off as a whole, and it reads from the environment so that a deployment whose config file is template-managed can still say so — MATOMO_JS_ENABLED=false. That matters for the failure mode a config edit has there: the next template sync reverts it silently, so the switch reads "off" until one day it does not, and nothing announces the change.

It governs both doors that put tracking into a page:

DirectiveGoverned byNotes
@matomoScriptjs.enabledthe tracker itself
@matomoNoscriptjs.enableda tracking pixel that works with JavaScript off
@matomoWebVitalsweb_vitals.enabledits own switch, and it ships off
@matomoOptOutnot tracking; it is the opt-out iframe and keeps working

The table is here because the list is easy to get wrong from memory: a consuming project's guard named two of the four, and the two it missed were the <noscript> pixel and the Web Vitals reporter — both real doors. If you are auditing which directives a codebase uses, read the registered list rather than writing it out.

heartbeat is what makes time-on-page meaningful. Without it Matomo can only measure the interval between two page views, so the last page of every visit counts as zero seconds. Fifteen seconds is a reasonable default; 0 disables it.

enable_link_tracking records outbound links and file downloads automatically. It is pushed after the page view, as Matomo requires.

noscript renders a tracking pixel for visitors without JavaScript. The pixel carries referrerpolicy="no-referrer-when-downgrade".

One caveat if you validate your markup, and it is the only reason the pixel can be split out: inside <head>, the HTML specification allows a <noscript> element to contain only <link>, <style> and <meta>. An <img> there is a parse error. Browsers recover from it — the parser's "after head" handling pushes the following head elements back where they belong — so the practical effect is a validator complaint, not a broken page, and only for visitors with JavaScript switched off.

If you would rather have neither, place the pixel in the <body>, where an <img> is legal. It is a two-step change:

// config/matomo-analytics.php
'js' => [
'noscript' => false, // stop @matomoScript emitting the pixel in <head>
],
<body>
{{-- ...your page... --}}
@matomoNoscript
</body>

@matomoNoscript renders exactly the same pixel and, like every other part, renders nothing at all when tracking is inactive. Leaving noscript at true and also placing the directive would emit it twice, so pick one.

dns_prefetch emits a resolver hint for the Matomo origin — and a second one for the asset host when js.host points somewhere else.

host loads matomo.js from a different origin (typically a Matomo Cloud CDN) while tracking stays on your own subdomain. See Matomo Cloud.

Content-Security-Policy nonce

Pass your nonce and it is applied to every script tag the directive renders:

@matomoScript($cspNonce)

Tag Manager instead of the tracker

Set js.tag_manager to a container URL and the directive renders the Matomo Tag Manager bootstrap (_mtm) instead of the direct tracker:

'js' => [
'tag_manager' => 'https://analytics.example.com/js/container_AbCdEf12.js',
],

Everything the container does is configured in Matomo's Tag Manager UI rather than here, and soft-navigation tracking becomes Tag Manager's job — it has its own history-change trigger.

Four of the js settings still apply, because they are not _paq pushes and have nothing to bootstrap against:

SettingUnder Tag Manager
js.enabledStill the master switch. MATOMO_JS_ENABLED=false renders nothing at all, container included.
js.noscriptStill emits the <noscript> pixel. This is deliberate: a visitor without JavaScript never loads the container either, so the pixel is the only thing that counts them. It hits matomo.php directly rather than going through the container — set 'noscript' => false if you would rather have no second path.
js.dns_prefetchStill emits the <link rel="dns-prefetch"> hints.
js.hostStill chooses the host those hints name. It no longer selects where a script is loaded from — the container URL is js.tag_manager in full.

The remaining six — enable_link_tracking, performance, custom_dimensions, content_tracking, heartbeat and the tracker half of js.host — are _paq pushes, and under Tag Manager there is no _paq bootstrap to push them onto. Configure those behaviors in the container instead.

Server-side tracking is unaffected either way.

The opt-out control

@matomoOptOut

This renders Matomo's own opt-out iframe, which lets a visitor opt out of tracking by this Matomo instance. It renders nothing when the instance is not configured.

The iframe is served by Matomo, so the cookie it sets lives on the Matomo domain — which server-side tracking cannot see. If you track server-side as well, pair it with the first-party opt-out cookie described in consent and redaction.

Also on the client side