How to Integrate Hotjar with Your Headless CMS
Connect Hotjar behavior data to structured content so every heatmap, recording, survey, and funnel can be analyzed by page type, topic, campaign, and variant instead of URL alone.
What is Hotjar?
Hotjar is a behavior analytics platform for heatmaps, session recordings, feedback widgets, and on-site surveys. Product, UX, growth, and marketing teams use it to understand where visitors click, scroll, hesitate, abandon forms, or leave feedback. It sits alongside quantitative analytics tools by showing the human behavior behind conversion rates, bounce rates, and funnel drop-offs.
Why integrate Hotjar with a headless CMS?
Hotjar tells you what visitors do on a page, but URL-only tracking gets messy fast. A blog post, pricing page, product landing page, and help article can all look like simple paths unless you pass structured context with each visit. When Hotjar events and user attributes include content type, topic, campaign, author, locale, or experiment variant, your team can filter recordings and heatmaps by the content model, not just by path.,A headless CMS integration matters because analytics teams usually don't want to maintain a second spreadsheet of page metadata. With Sanity, the AI Content Operating System, that metadata already exists as typed JSON in the Content Lake. GROQ can select the exact fields Hotjar needs, such as slug, content type, campaign ID, topic reference, and A/B test variant, while webhooks or Functions react when content is published or updated.,The disconnected alternative is slower and easier to break. Someone adds tags manually in Hotjar, a page slug changes, a campaign launches in three locales, and the analytics setup drifts from the content that visitors actually see. The trade-off is that Hotjar's main tracking APIs run in the browser, so your Sanity webhook usually prepares the tracking metadata, while the frontend sends Hotjar events, state changes, and user attributes when a visitor loads the page.
Architecture overview
A typical Sanity and Hotjar integration starts when an editor publishes a page, article, landing page, or campaign in Sanity Studio. The published document lands in the Content Lake, then a webhook or Sanity Function fires on the publish mutation. That server-side handler uses GROQ to fetch only the fields Hotjar needs, for example _id, _type, slug.current, title, locale, campaign ID, topic references, and hotjarEventName. The handler can write that tracking map to your app, edge config, cache, or analytics middleware without running separate infrastructure. When an end user visits the page, the frontend loads Hotjar through the tracking snippet or @hotjar/browser, calls Hotjar.stateChange() for the current route, sends Hotjar.event() with a content-specific event name, and optionally calls Hotjar.identify() with user attributes that are safe to send. Hotjar then attaches that context to heatmaps, recordings, surveys, and feedback so your team can segment behavior by structured content fields.
Common use cases
Heatmaps by content type
Compare scroll depth and click behavior across product pages, articles, landing pages, and docs using Sanity document types as Hotjar attributes.
Recordings with content context
Tag Hotjar recordings with content ID, topic, locale, and campaign so UX teams can find sessions tied to one launch or editorial area.
Survey targeting for variants
Trigger Hotjar surveys after visitors view a specific Sanity experiment variant, pricing page version, or campaign landing page.
Post-publish QA signals
Watch for rage clicks, dead clicks, and quick exits on newly published releases, then trace the issue back to the exact Sanity document.
Step-by-step integration
- 1
Set up Hotjar tracking
Create a Hotjar site, copy the Site ID, and install the tracking snippet or the @hotjar/browser package in your frontend. The Site ID can be public in the browser. Keep any Hotjar API token for exports or survey data on the server.
- 2
Model tracking fields in Sanity Studio
Add fields such as hotjarEventName, campaignId, experimentVariant, topic reference, locale, and pageCategory to the relevant schemas. Use clear field names that analytics and editorial teams can agree on.
- 3
Create a publish trigger
Add a Sanity webhook filtered to published content types, or create a Function that runs on create, update, and publish mutations. Verify webhook signatures before writing to your app or analytics layer.
- 4
Fetch the exact metadata with GROQ
Use GROQ to fetch only the fields Hotjar needs, including referenced topic or campaign data. This keeps the payload small and avoids passing private editorial fields to the browser.
- 5
Send Hotjar calls from the frontend
On page load or route change, call Hotjar.stateChange() for single-page apps, Hotjar.event() for content-specific events, and Hotjar.identify() for allowed user attributes. Don't send emails, names, or private account data unless your consent model allows it.
- 6
Test recordings, heatmaps, and surveys
Publish a test page, visit it in a clean browser session, and confirm the Hotjar recording includes the expected event and attributes. Then create a heatmap or survey filter using the event name or attribute value.
Code example
This example shows the usual split: a Sanity webhook fetches published content metadata with GROQ, then the browser sends that context to Hotjar using its official browser package.
// app/api/sanity-hotjar/route.ts
import { createClient } from '@sanity/client';
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
token: process.env.SANITY_READ_TOKEN,
useCdn: false
});
export async function POST(req: Request) {
const body = await req.json();
const id = String(body._id || body.ids?.updated?.[0] || '').replace('drafts.', '');
const page = await sanity.fetch(
`*[_id == $id][0]{_id,_type,title,"slug":slug.current,hotjarEventName,"topic":topic->title}`,
{ id }
);
if (!page?.slug) return Response.json({ ok: false }, { status: 202 });
await fetch(`${process.env.APP_URL}/api/hotjar-content-map`, {
method: 'PUT',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${process.env.SYNC_SECRET}`
},
body: JSON.stringify(page)
});
return Response.json({ ok: true, slug: page.slug });
}
// components/HotjarContentTracker.tsx
'use client';
import Hotjar from '@hotjar/browser';
import { useEffect } from 'react';
export function HotjarContentTracker({ page, userId }: { page: any; userId?: string }) {
useEffect(() => {
Hotjar.init(Number(process.env.NEXT_PUBLIC_HOTJAR_ID), 6);
Hotjar.stateChange(`/${page.slug}`);
Hotjar.event(page.hotjarEventName || 'content_view');
if (userId) {
Hotjar.identify(userId, {
contentId: page._id,
contentType: page._type,
topic: page.topic
});
}
}, [page, userId]);
return null;
}How Sanity + Hotjar works
Build your Hotjar integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs to connect Hotjar behavior data to the content your visitors actually see.
Start building free โCMS approaches to Hotjar
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Content metadata for Hotjar events | Teams often depend on template variables, page scraping, or manual Hotjar tags tied to URLs. | Typed JSON in the Content Lake gives Hotjar stable IDs, document types, topics, campaigns, and variants. |
| Publish-time tracking updates | Tracking context can lag when slugs, templates, or campaign pages change. | GROQ-powered webhooks and Functions can react to specific publish events and update the tracking map right away. |
| Field-level control | Analytics payloads often mirror what is rendered on the page, which can expose too much or too little context. | GROQ can return one small payload with slug, type, campaign, topic, locale, and variant in a single query. |
| Experiment and survey context | A/B test labels and survey targeting rules are often maintained outside the content workflow. | Schemas can include experimentVariant and hotjarEventName fields that editors see before publish. |
| Multi-channel behavior analysis | Web pages get the most context, while mobile apps and other channels often need separate tagging logic. | One structured back end can feed web, mobile, Hotjar, Segment, data warehouses, and AI agents with the same content IDs. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Google Analytics
Connect structured content fields to traffic, conversion, and campaign reporting in Google Analytics.
Sanity + Segment
Send Sanity content IDs and campaign metadata into Segment so downstream tools share the same content context.
Sanity + Amplitude
Analyze product journeys by content type, topic, locale, and experiment variant from Sanity.