How we deal with broken Matomo heatmap screenshots

Heatmap screenshots break on most modern sites because of how Matomo serializes and re-renders the DOM. Here are the patterns we kept running into, and a small open source extension we built to handle them.

If you've opened a Matomo heatmap and found a blank rectangle where your hero image should be, or a product grid that cuts off after the first row, or a sticky header sitting square on top of the page content, you're not the first. Heatmap screenshots break on most modern websites out of the box. The reason is structural, not something you fix by tweaking your tracking config.

Matomo's tracking script serializes your page's DOM into HTML and sends it to your Matomo server. The server then renders that HTML again, in a completely different context, to produce the screenshot you see. No cookies. No session. Different origin. None of the runtime your JavaScript framework set up. A lot of things that worked because they were running in a real browser quietly fail when the same HTML gets re-rendered cold somewhere else.

Once you see that, the bug list starts making sense.

The patterns we keep running into

Most sites hit at least three of these. Some hit all six.

  • CORS blocks your CDN-hosted images and fonts, because the Matomo server has no credentials to your domain. Images come back as broken icons. Fonts fall back to system defaults.
  • Scroll containers (overflow: hidden or overflow: auto with a fixed height) clip everything below their visible area. Matomo serializes the DOM in its current state, so what wasn't on screen at capture time isn't in the snapshot.
  • Sticky and fixed headers collapse onto the content underneath. Their positions are calculated against a viewport, and there's no viewport server-side, so they drop down to wherever they sit in the DOM.
  • Relative URLs like /images/hero.jpg resolve against the Matomo server instead of yours. The file doesn't exist there, so it just fails silently.
  • Custom fonts loaded via @font-face fall back to system defaults when the font files are CORS-protected or block the Matomo server's user-agent.
  • Single-page apps get captured before React or Vue have finished rendering, leaving half the page missing from the snapshot.

Most of these have infrastructure-level fixes. Add CORS headers on your CDN. Switch to absolute URLs. Use a matomoHeatmap CSS hook to override sticky positioning during capture. Matomo's own docs cover several of them, and we've written a longer post that walks through each issue and how to fix it server-side if that's the route you want.

The catch is that the work spreads across CDN config, CSS, and your tracking setup, and some of it you can't fix at all if you don't control the assets. We hit that wall on enough client sites that we ended up doing something else.

What we ended up building

We started with a small script we'd paste into DevTools before each capture. It embedded images and fonts as data URIs, expanded scroll containers, unstuck headers, rewrote relative URLs to absolute. Over time it grew into a Chrome extension we used internally on every client.

That's what Matomo Heatmap Helper is. We open sourced it.

Setup is one screen. You enter your Matomo URL, an API token, and pick which sites you want the toolbar to appear on. On any matching page, a small bar shows up.

The work happens at capture time. When you press the screenshot button, the extension does the things that are time consuming to do statically. It embeds images and fonts as base64 data URIs so the Matomo server never has to fetch anything cross-origin. It expands scroll containers to their full content height. It converts sticky and fixed headers to normal flow. It rewrites relative URLs to absolute, resizes iframes, pauses videos to frame zero, and gives SPA content time to render. Then it calls Matomo's screenshot API, waits for the result, and reverses every change. Your live page goes back to how it was.

The reason this works is that you can interact with elements before capture. Custom scroll containers, a misbehaving sticky header, a section where assets aren't loading: you click them in Interactive mode, they get a lock icon, and the pipeline knows what to do with them. The tracking script alone can't do that, because it captures whatever state the DOM happened to be in at the moment it ran. Letting a person tell the extension which elements need handling is what fixes the cases the script never could.

The extension only talks to your Matomo instance. Your API token sits in chrome.storage.local, which is isolated from the pages you visit. No telemetry, no usage tracking. The code is on GitHub. Issues and pull requests are welcome.

Why we put it out

As an Official Matomo Partner agency, broken heatmaps were a problem we hit on almost every client engagement. The events were tracked correctly, but the screenshots were unusable, which is the part most stakeholders actually look at.

Matomo has given us a lot as an agency. We use the extension daily on client work, and we continue improving edge cases we find on websits. If you encoutner any unresolved heatmap issues, please open a github issue and we'll see if we can add a fix to the extension.

What we're working on

Matomo Heatmap Helper grew out of a larger project. Martez connects Matomo with Meta Ads and Google Ads, so ROAS, CLV, and multi-touch attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. If that's relevant to you, you can join the waitlist.

Either way, the heatmap extension is its own thing, and it stays that way. We built it because we needed it.