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>
It records a page view on every page it is on, so leave the server-side page view
middleware, matomo.track, off those pages: the two together count each view twice.
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. It renders nothing either on a page that tracking.environments or
tracking.except_routes excludes; which other gate
rules reach matomo.js is on the gate's page.
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:
disableCookies— whenprivacy.cookielessis true (the default).- The consent call for your
privacy.consentposture, if any. setDoNotTrack— whenprivacy.honor_dntis true (the default).setCustomRequestProcessing, which redacts the names inprivacy.redact.query_paramsand the token of a password-reset link from every request — when redaction is on (the default).- Any statically configured custom dimensions.
disablePerformanceTracking— only whenjs.performanceis false.trackPageView.- Content-impression scanning, if content tracking is on.
enableLinkTracking— whenjs.enable_link_trackingis true (the default), then the heartbeat timer — whenjs.heartbeatis above zero.- The tracker URL and site id, then the loader. It inserts matomo.js at once, or, on a page the browser prerenders, when the reader opens it, so a prerender nobody opens is not counted. See Prefetched and prerendered pages.
- 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:
| Directive | Governed by | Notes |
|---|---|---|
@matomoScript | js.enabled | the tracker itself |
@matomoNoscript | js.enabled | a tracking pixel that works with JavaScript off |
@matomoWebVitals | web_vitals.enabled | its own switch, and it ships off |
@matomoOptOut | — | not tracking; it is the opt-out control 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, for a request the
tracking gate lets through. The browser requests the pixel from
Matomo directly, so consent under full, an opt-out and an excluded route are checked when the
page is rendered. The pixel
names the page itself in url, redacted like a server-side hit, and sends no Referer:
without url, Matomo takes the page from that header, and the browser would send the full
address with its query.
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. The container is loaded the way matomo.js is: at once, or, on a prerendered page, when the reader opens it.
Four of the js settings still apply, because they are not _paq pushes and
have nothing to bootstrap against:
| Setting | Under Tag Manager |
|---|---|
js.enabled | Still the master switch. MATOMO_JS_ENABLED=false renders nothing at all, container included. |
js.noscript | Still 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_prefetch | Still emits the <link rel="dns-prefetch"> hints. |
js.host | Still 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 JavaScript opt-out: an empty <div> and the script from your Matomo
instance that fills it with the opt-out text and checkbox. It renders nothing when the
instance is not configured. Under a strict content security policy, pass the nonce, as for
the other directives: @matomoOptOut($nonce).
The opt-out is a first-party cookie, mtm_consent_removed, which the script writes through
the tracker on the page. matomo.js writes it with cookies disabled as well and stops tracking
once it is there. Matomo's older iframe opt-out set its cookie on the Matomo domain instead,
which a browser that blocks third-party cookies never keeps when Matomo runs on another site.
The tracking gate reads the same cookie, so an opt-out through this control stops server-side
tracking as well: page views, beacons and the hits your code sends. The package exempts the
cookie from EncryptCookies, because JavaScript writes it in plain text. The first-party
opt-out cookie described in consent and redaction stays
the way to opt a visitor out from your own server code.
Also on the client side
- SPA and soft navigation — virtual page views for Livewire, Inertia and History-based routers.
- Page performance — what
js.performancecontrols. - Web Vitals — the separate
@matomoWebVitalsdirective.