Plugin screenshot thumbnail 1/7
Plugin screenshot thumbnail 2/7
Plugin screenshot thumbnail 3/7
Plugin screenshot thumbnail 4/7
Plugin screenshot thumbnail 5/7
Plugin screenshot thumbnail 6/7
Plugin screenshot thumbnail 7/7

Jarhead

Hotjar for Craft CMS. Paste the Site ID; every front-end page gets the tracking code, with no template change.

Reference point: the WordPress plugin Hotjar. That plugin is one field and one wp_head hook, and for a single-site WordPress blog it is genuinely enough. Craft sites are not that. Jarhead does the one field too — and then does the four things that go wrong on the day after you install it.

Free. Single edition, everything switched on, no licence key.


Install

composer require justinholtweb/craft-jarhead
php craft plugin/install jarhead

Craft 5.3+, PHP 8.2+. No runtime dependencies beyond Craft's own. No database tables.

Then: Settings → Plugins → Jarhead, paste your Hotjar Site ID, done.


The four things

1. Half your recordings are your own team

Hotjar bills by session. An editor clicking through drafts, a designer refreshing a template, an admin previewing an entry — all of it records, all of it counts, none of it is a customer.

Jarhead excludes admins and previews by default, and will also exclude signed-in users, named user groups, and any URI you name.

'excludeAdmins' => true,
'excludePreviews' => true,
'excludedGroups' => ['editors', 'staff'],
'uriRules' => [
    ['pattern' => 'checkout/*', 'mode' => 'exclude'],
    ['pattern' => 'account/*', 'mode' => 'exclude'],
],

It also refuses to run under an automated browser — Playwright, Puppeteer, Selenium, most uptime checkers. That one is decided in the visitor's browser rather than from the user agent, because a user-agent test on the server would vary your page cache.

2. The Site ID ships to every environment

One ID in one field, deployed to staging, to .ddev.site, to a client's UAT box. Every recording of a developer typing asdf into a form lands in the same account as the real ones, and nothing in the data tells them apart afterwards.

// config/jarhead.php
return [
    'siteIds' => ['default' => '$HOTJAR_SITE_ID'],
    'allowedEnvironments' => ['production'],
];

The Site ID is per Craft site and read through App::parseEnv(), so production and staging can have different Hotjar accounts — or staging can have none. An environment variable that does not exist in this environment is treated as "not configured here", not as a literal string, so the same project config is correct everywhere.

php craft jarhead/status --strict exits non-zero when a site that should be sending is not, which makes it a post-deploy check rather than something a stakeholder tells you about.

3. Consent is bolted on afterwards, if at all

Session recording is personal data by any reading of GDPR — it is a video of somebody using your site. Under a consent gate, nothing reaches Hotjar until the visitor agrees: no script element, no request to static.hotjar.com, no _hjSettings. Not a blocked script and not a deferred one — the tracking code is created by the gate, in the browser, after consent. (The gate itself is in the page, and it holds Hotjar's URL as a string it has not used.)

'consentMode' => 'cookie',
'consentCookie' => 'cookie_consent',
'consentCookieValue' => 'substring:analytics',

Four gates:

ModeBoots when
cookiea named cookie exists, optionally holding a named value
eventa DOM event fires on window or document
dataLayerdataLayer carries analytics_storage: 'granted' — Google Consent Mode v2
manualyour code calls window.jarhead.consent()

Do Not Track and Global Privacy Control are honoured on top of whichever gate you pick, and both are read in the browser for the same caching reason.

Anything you call while the gate is shut is queued, not lost — an event written into a template is earlier than the visitor's decision by definition.

4. A recording with no tags is a video you have to watch

Every Craft page already knows its section, its entry type, its template, its site, and whether the viewer was signed in. Jarhead sends the ones you switch on, as Hotjar attributes, so recordings and heatmaps can be filtered:

'autoAttributes' => ['craft_site', 'craft_section', 'craft_entry_type', 'craft_template'],
'customAttributes' => [
    ['name' => 'release', 'value' => '$RELEASE_TAG'],
],

Signed-in users can optionally be identified by their Craft user ID, HMAC'd with your security key by default. There is no setting anywhere that sends an email address or a username — a Hotjar account is not the right home for either, and an identifier only has to be stable to be useful.


Twig

Everything is safe to call on a page that is not being tracked; you never have to ask first.

{# Place the tracking code yourself. Doing this stands automatic injection down for the page,
   so you can leave the setting on. #}
{{ craft.jarhead.snippet }}

{# Is this page tracked, and if not, why not? #}
{{ craft.jarhead.enabled }}
{{ craft.jarhead.explain.reason }}     {# 'preview', 'uri-excluded', 'environment', … #}
{{ craft.jarhead.explain.message }}    {# a sentence you can put in a staging footer #}

{# Events and attributes. Queued if the consent gate is still shut. #}
{{ craft.jarhead.event('Newsletter signup') }}
{{ craft.jarhead.identify(null, { plan: 'pro', trial: 'no' }) }}
{{ craft.jarhead.tag(['checkout', 'guest']) }}

{# The raw snippet, for pasting into a tag manager. No gate, no exclusions, no attributes. #}
{{ craft.jarhead.trackingCode() }}

JavaScript

window.jarhead.consent();                       // grant consent (the only way in under `manual`)
window.jarhead.event('Added to cart');
window.jarhead.identify('user-123', { plan: 'pro' });
window.jarhead.tag(['checkout']);
window.jarhead.stateChange('/checkout/step-2'); // a route change Jarhead did not see
window.jarhead.ready(function () { /* Hotjar is loaded */ });
window.jarhead.status();                        // why it has or has not booted

status() is the first thing to type into a console when a page is not recording:

{ booted: false, blockedBy: 'gpc', consentMode: 'cookie', hotjarSiteId: 1234567, queued: 2 }

The Hotjar utility

Utilities → Hotjar. Three questions, in the order they actually get asked:

  1. Is it configured? Every Craft site, its resolved Site ID, whether that came from an environment variable, and the verdict for its homepage right now.
  2. Does Hotjar know this ID? A button that asks Hotjar's CDN for the tracking script belonging to your Site ID, and reads the answer properly. Hotjar returns 200 application/javascript for every numeric ID ever asked for — an ID that does not exist gets 200 with an empty body — so a check written against the status code says yes to hotjar-1.js. Jarhead reads the body.

    The script also carries your site's Hotjar settings, so this reports the two things that actually explain an empty account: recording switched off for the site, and sampling below 100%. Neither is an installation problem, and both look exactly like one.

    This is the only outbound request Jarhead ever makes, it happens only when somebody presses the button, and it goes to static.hotjar.com and nowhere else.

  3. Why is it not on that page? Type a URI, optionally a user ID and a preview flag, and get back the first rule that applied — through the same code path a real request takes.

The utility also prints the Content Security Policy directives Hotjar needs, because a CSP is the single most common reason a correctly installed tracking code does nothing and reports it to a console nobody is looking at.


Console

php craft jarhead/status                 # what each site would do right now
php craft jarhead/status --strict        # non-zero exit if a site is not sending — for CI
php craft jarhead/verify                 # ask Hotjar whether the Site IDs exist
php craft jarhead/explain checkout/cart  # why that URI would or would not be tracked
php craft jarhead/explain blog --user=14 # …as a specific user
php craft jarhead/snippet --site=default # the raw tracking code

What Jarhead never does

  • It never tracks the control panel, and there is no setting for it. A session recorder pointed at the control panel records other people's addresses, order histories and account details. That is a data breach with a subscription.
  • It never changes Hotjar's snippet. The tracking code is emitted verbatim, because it is the documented integration surface — Hotjar's own support will ask you to compare it — and a plugin that "improves" it is a plugin whose bug reports all close as "not our code". Jarhead changes what runs around the snippet.
  • It never stores anything about a visitor. No tables, no counters, no logs of who was tracked. Hotjar is the thing collecting the data; a shadow copy in Craft would be a second privacy problem added to solve the first.
  • It never sends an email address or a username.

Settings reference

Every setting is overridable from config/jarhead.php.

SettingDefault
enabledtrueMaster switch
siteIds[]Hotjar Site ID per Craft site handle; $ENV_VAR supported
snippetVersion6Hotjar's hjsv
autoInjecttrueSplice into front-end HTML automatically
injectionPoint'head'head or body; falls back if the tag is absent
scriptNonce''CSP nonce, usually an environment variable
allowedEnvironments[]Empty means all of them
trackInDevModefalse
excludePreviewstrueLive preview, drafts, share tokens
excludeAdminstrue
excludeLoggedInfalse
excludedGroups[]User group handles
uriRules[]['pattern' => …, 'mode' => 'include'\|'exclude']; re: for regex
excludeAutomatedBrowserstruenavigator.webdriver
consentMode'off'off, cookie, event, dataLayer, manual
consentCookie''
consentCookieValue''Comma separated; substring: prefix supported
consentEvent'jarhead:consent'
consentDataLayerKey'analytics_storage'
consentDataLayerValue'granted'
consentPollInterval500Milliseconds, cookie mode only
consentTimeout0Seconds; 0 watches for the life of the page
honourDnttrue
honourGpctrue
autoAttributessite, section, entry type, environment
customAttributes[]['name' => …, 'value' => …]; $ENV_VAR supported
identifyUsersfalse
hashUserIdstrue
spaSupportfalse

URI patterns are globs by default (blog/*), regular expressions when prefixed with re:, and __home__ matches the homepage. An exclusion always beats an inclusion regardless of order, because these are read as "everywhere, except there".


Licence

The Craft License. See LICENSE.md. Jarhead is free: no editions, no licence key, and no licensing code in the plugin.

Jarhead is not affiliated with, endorsed by, or sponsored by Hotjar Ltd. "Hotjar" is their trademark; this plugin only installs the tracking code they publish.

Installation Instructions

To install this plugin, copy the command above to your terminal.

Reviews

This plugin doesn't have any reviews.

Active Installs
0
Version
5.0.0
License
Craft
Compatibility
Craft 5
Last release
August 23, 2026
Categories