Why your Matomo heatmap screenshots are broken (and how to fix them)

Matomo heatmap screenshots break because your page gets re-rendered on a different server without cookies, fonts, or CORS access. Here's what causes each issue and how to fix it.

Matomo's screenshot capture is a two-step process, and the gap between those steps is where things go wrong.

First, when someone visits your page, Matomo's tracking JavaScript serializes the DOM into an HTML string (using a library called TreeMirror) and sends it to your Matomo server. Then, Matomo's server renders that HTML to produce the background image you see in the heatmap view.

The problem is your page is being re-rendered in a completely different context. The Matomo server has no cookies, no sessions, a different origin, and no idea what your JavaScript frameworks did to the page after load. Everything that worked because it was running in a real browser with your domain's credentials can break when that same HTML gets rendered cold on a different server.

Once you understand this, most heatmap screenshot bugs start to make sense.

What broken heatmap screenshots look like

These are the failures people typically run into:

  • Hero image loads fine in the browser, shows up as a broken icon in the heatmap
  • A scrollable product grid only shows its first row, the rest clipped
  • Sticky navigation overlaps the page content in the screenshot
  • Brand fonts replaced by generic system defaults
  • Assets with relative paths like /assets/banner.webp fail to load entirely
  • SPA pages captured mid-render with half the content missing

All of these have specific technical causes. Let's go through them.

CORS blocking your images and fonts

This is the most common issue. Your hero image, product photos, or background images show up as empty boxes or broken icons in the heatmap screenshot.

It happens because your images load in the browser just fine, since the browser has your cookies and session context. But when Matomo's server tries to load https://cdn.yoursite.com/hero.jpg, the CDN sees an unfamiliar origin and refuses the request. Classic CORS problem.

The server-side fix is straightforward: add Access-Control-Allow-Origin: * to your CDN's response headers. Matomo's docs cover this in their heatmap setup guide.

If you don't control the CDN or don't want to change server config, the alternative is embedding resources directly into the DOM as base64 data URIs before Matomo captures the page. That way the serialized HTML carries every image inline and the server never needs to fetch anything. This applies to src, srcset, CSS background images, SVG references, and video poster images.

Scroll containers cutting off your content

You have a section with a fixed height and a scrollbar. In the heatmap screenshot, only the visible portion renders. Everything below the fold is gone.

Elements with overflow: hidden or overflow: auto clip content to their rendered height. Matomo serializes the DOM at its current visual state, so the captured HTML reflects clientHeight, not the full scrollHeight.

To fix this yourself, avoid fixed heights on content containers. Use min-height instead of height, and don't apply overflow: hidden on elements that contain scrollable content.

Sticky and fixed headers overlapping the page

Your sticky navigation works perfectly in the browser, but in the screenshot it just sits on top of the content, covering whatever is underneath.

position: fixed and position: sticky elements are positioned relative to the viewport. Server-side, there is no viewport, so they collapse onto the content below them.

Matomo adds a matomoHeatmap class to the <html> element during server-side screenshot rendering. You can use this to write CSS rules that only apply during capture. For example:

css
html.matomoHeatmap .your-sticky-header {
  position: relative;
  top: auto;
}

This converts your sticky header to normal flow in the screenshot without affecting how the page looks for real users. Matomo's docs cover this in their FAQ on fixing overlapping headers in heatmaps and applying custom stylesheets during capture.

If you'd rather not maintain those extra CSS rules, the general approach is to detect header and nav elements with fixed or sticky positioning, convert them to position: relative, clear their top/bottom/left/right values, and for fixed elements, insert a placeholder div to keep the layout from shifting. After capture, everything gets restored.

Relative URLs that stop working

Some images and stylesheets load fine on your site but are missing in the Matomo screenshot, even though they're not cross-origin. This one is confusing until you realize what's going on.

Your page uses relative URLs like /images/banner.jpg. In the browser, that resolves against your domain. In Matomo's rendering context, /images/banner.jpg resolves against Matomo's server. The file doesn't exist there.

Fix it by using absolute URLs everywhere, or by adding a <base href="https://yoursite.com/"> tag to your <head>. If neither is practical, a pre-capture script can scan the document for relative URLs in src, href, srcset, and CSS background-image attributes and rewrite them to absolute URLs using the page's current origin.

Custom fonts showing up as system defaults

Your brand fonts are gone. The screenshot shows whatever system font the server has, which is usually something ugly.

@font-face rules reference font files hosted on CDNs or your server. When Matomo's server tries to load those files, it gets blocked by CORS or the CDN requires a browser user agent. The server falls back to a default.

The fix: host fonts on a server with permissive CORS headers. Google Fonts already does this, but many self-hosted fonts don't. If changing headers isn't an option, you can embed fonts directly in the page by fetching the font files, converting them to base64 data URIs, and injecting @font-face rules with those data URIs into <style> tags in the <head>. That way the serialized HTML carries its own font definitions and renders correctly regardless of where it's served from.

Iframes, videos, and SPAs

A few more edge cases worth knowing about.

Iframes often have fixed CSS dimensions that don't match their actual content. For same-origin iframes you can read the real content height from contentDocument.body.scrollHeight and resize accordingly. For cross-origin iframes, you'll need a generous fallback height.

Auto-playing videos get captured mid-playback, showing a random frame. Pause them and seek to frame 0 before capture so the screenshot shows the first frame.

Single page applications are the hardest. React, Vue, Angular apps load content dynamically, and Matomo might capture the DOM before your framework finishes rendering. The URL might not match what your router displays, either. There's no single fix. Your best bet is configuring Matomo's URL matching rules for your SPA routes and using Matomo's JS API to trigger recording after render completes.

Fixing everything at once with Matomo Heatmap Helper

Most real sites have several of these problems simultaneously. Fixing them all manually means touching CDN configs, rewriting URLs, refactoring CSS, and writing custom capture scripts.

Matomo Heatmap Helper is a free, open source extension for Chrome and Firefox that handles all of the above. Source on GitHub.

Here's how it works in practice. You install the extension, enter your Matomo URL and API token, and pick which sites to enable. On matching pages, a toolbar appears. Click "Interactive" and then click the elements that need fixing: scroll containers, sticky headers, sections with missing images. They get a lock icon to show they'll be processed.

When you hit the screenshot button, the extension runs through a sequence. It fetches and embeds cross-origin images and fonts first, then expands constrained elements and unsticks headers, handles iframes and videos, converts relative URLs to absolute, and triggers Matomo's screenshot API. After capture, every change is reversed. Your page goes back to how it was.

Before you take a screenshot

A quick checklist:

  1. Make sure the page has fully rendered, especially SPA content and lazy loaded sections
  2. Lock containers that clip content or need expansion
  3. Check for sticky or fixed headers that might overlap content
  4. Verify your key images and fonts are showing up
  5. Take the screenshot and confirm verification succeeds
  6. Open the heatmap view and check the result before starting analysis

Two paths forward

These screenshot issues aren't bugs in your site. They're side effects of Matomo serializing your page and re-rendering it somewhere else. Any site using CDNs, web fonts, scroll containers, or sticky headers will hit some combination of them.

You can fix each issue at the infrastructure level: add CORS headers, use absolute URLs, refactor overflow containers, toggle sticky positioning during capture. That's the right long term approach if you control the server, but it takes time and some issues don't have clean server-side solutions.

Or you can fix them at capture time with Matomo Heatmap Helper. Use the interactive mode to tell it which parts of your page need help, and get clean screenshots without touching production code.