Waarom je sticky header herhaalt (of bevriest) in je Matomo heatmap (en hoe je dat oplost)

Fixed en sticky headers herhalen zich over de pagina of bevriezen ergens in het midden van Matomo heatmap-screenshots, omdat Matomo de vastgelegde DOM rendert als één tall image zonder consistente viewport om in te verankeren. Dit is waarom het gebeurt en hoe we ermee omgaan.

Als je een Matomo heatmap hebt geopend en je header twee, drie, soms zes keer als een postzegel over de pagina gestempeld zag staan, of één exemplaar ervan bevroren midden over de content eronder, dan kloppen de klikken zelf gewoon. Matomo heeft ze op de juiste coördinaten vastgelegd. Wat kapot is, is de screenshot eronder. Je sticky header is niet gerenderd zoals die in een browser doet, waardoor die zich herhaalt bij elke scrollpositie of zichzelf vastzet op waar de renderer toevallig naar keek. In beide gevallen worden alle klikken eronder geplot bovenop nav-links, en verandert een stuk van je echte heatmap in één grote dode zone.

De snelste fix is een gratis Chrome-extensie die we onderhouden, genaamd Matomo Heatmap Helper. Die detecteert elke fixed en sticky header op de pagina, zet ze tijdens het vastleggen om naar position: relative met een placeholder van dezelfde hoogte, en zet ze daarna terug. De rest van dit artikel beschrijft hoe je hetzelfde doet zonder extensie, plus een permanent CSS-patroon dat de moeite waard is om te shippen als je regelmatig heatmaps vastlegt.

Hoe je het oplost zonder de extensie

Er is een console-snippet die het probleem oplost vlak vóór elke capture, en een permanente CSS-branch die je met de site kunt shippen. Kies wat past bij de situatie waar je mee werkt.

Vind elke fixed en sticky header op de pagina

Voordat je iets wijzigt, check je wat er eigenlijk verankerd is. Open de pagina in Chrome, druk op F12 om DevTools te openen, ga naar de Console en plak:

js
// Logs every header-like element and its computed position value
document.querySelectorAll('header, nav, [role=banner], [role=navigation], .header, .navbar, .nav, .sticky')
  .forEach(el => console.log(getComputedStyle(el).position, el));

Alles wat fixed of sticky rapporteert is verdacht. Op de meeste marketingsites vind je er meer dan één. De hoofdheader is fixed, de cookiebalk is fixed, een "terug naar boven"-knop is fixed, een secundaire sub-nav is sticky. Ze stapelen allemaal en breken de screenshot elk op hun eigen manier.

Maak elke header los vlak vóór het vastleggen

Plak dit voordat je de heatmap-capture triggert. Het spiegelt wat de sticky-header-fixer van de extensie doet: zet elke fixed en sticky header om naar position: relative, en voeg voor fixed-headers (die los staan van de documentflow) een onzichtbare placeholder met dezelfde hoogte in, zodat de scrollpositie en klikcoördinaten niet verschuiven.

js
// Paste into the browser console. Unsticks every fixed/sticky header so it
// doesn't repeat or float across the heatmap, and inserts a placeholder
// with the same height/width to preserve the rest of the layout.
(() => {
  const SELECTOR = 'header, nav, [role=banner], [role=navigation], .header, .navbar, .nav, .sticky';
  let unstuck = 0;
 
  document.querySelectorAll(SELECTOR).forEach(el => {
    const cs = getComputedStyle(el);
    if (cs.position !== 'fixed' && cs.position !== 'sticky') return;
 
    // Capture original dimensions before changing position
    const rect = el.getBoundingClientRect();
 
    // Insert invisible placeholder for fixed (sticky already takes layout space)
    if (cs.position === 'fixed' && el.parentElement) {
      const ph = document.createElement('div');
      ph.style.height = rect.height + 'px';
      ph.style.width = rect.width + 'px';
      ph.style.visibility = 'hidden';
      ph.dataset.heatmapPlaceholder = 'true';
      el.parentElement.insertBefore(ph, el);
    }
 
    // Convert to relative so it joins document flow and stops repeating
    el.style.position = 'relative';
    el.style.top = 'auto';
    el.style.bottom = 'auto';
    el.style.left = 'auto';
    el.style.right = 'auto';
    el.style.zIndex = 'auto';
    el.style.width = 'auto';
    unstuck++;
  });
 
  console.log(`Unstuck ${unstuck} headers. Now trigger your Matomo heatmap capture.`);
  console.log('To revert: location.reload(), or remove the placeholders by hand.');
})();

Voer het uit, kijk naar de teller, trigger de capture. Als het Unstuck 0 headers logt maar je nog steeds een herhalende header op de pagina ziet, mist de selector-lijst hierboven welke class je header daadwerkelijk gebruikt. Inspecte het element, zoek het wrapper-element met position: fixed, en voeg de selector ervan toe aan SELECTOR.

Of verberg de header gewoon tijdens het vastleggen

Een heatmap op een sticky header vertelt je toch zelden iets nuttigs. Bezoekers scrollen, de header beweegt mee, en elke klik op de nav landt op een andere pagina-Y-coördinaat. Het resultaat is een heatmap van de nav die over de volledige paginahoogte is uitgesmeerd. Als de header niet is wat je wilt onderzoeken, is de simpelste aanpak om die helemaal buiten beeld te laten:

js
// Hides every header during the capture
document.querySelectorAll('header, nav, [role=banner], .navbar, .sticky')
  .forEach(el => el.style.display = 'none');

De capture komt terug alsof de header er niet was. Echte klikken eronder landen nog steeds op de juiste elementen, want de klikcoördinaten veranderen niet. Je verliest alleen de (grotendeels nutteloze) heatmap-data op de nav zelf.

Permanente CSS-fix gescoped op capture-modus

Als je heatmaps vastlegt op dezelfde pagina's herhaaldelijk, word je moe van het console-plakken. Beter is een CSS-branch die alleen activeert als je de URL bezoekt met een vlag zoals ?matomo_heatmap=1. Voeg een class toe aan <html> als de vlag aanwezig is:

js
// Add to your site (or your tag manager)
if (new URLSearchParams(location.search).has('matomo_heatmap')) {
  document.documentElement.classList.add('heatmap-mode');
}

Ship dan CSS die elke header loskoppelt binnen die class:

css
/* Edit selectors to match your site's header markup */
html.heatmap-mode header,
html.heatmap-mode nav,
html.heatmap-mode [role=banner],
html.heatmap-mode .navbar {
  position: relative !important;
  top: auto !important;
  left: auto !important;
  right: auto !important;
  bottom: auto !important;
  width: auto !important;
  z-index: auto !important;
}

De capture-workflow is nu gewoon: bezoek ?matomo_heatmap=1, trigger de heatmap, klaar. Geen console-plakken, geen risico op vergeten. Het nadeel is dat de placeholder-truc (de originele layout bewaren voor position: fixed-headers) moeilijker te uitdrukken is in pure CSS, dus de pagina kan licht verschuiven als je de class inschakelt. Voor de meeste sites is dat prima. De klikdata is wat telt, en de visuals blijven dicht genoeg bij de echte layout om leesbaar te zijn.

Waarom Matomo sticky headers niet goed kan renderen

Matomo's heatmap-renderer neemt de geserialiseerde DOM en rasteriseert die als één tall image — de volledige hoogte van de pagina, van de bovenkant van <body> helemaal naar beneden. Er zit geen scrollen in die pipeline. Er is geen viewport zoals een echte browser die heeft. Het is één groot canvas.

Een header met position: fixed staat los van de documentflow en is verankerd aan wat de huidige viewport is. Als de renderer één tall canvas produceert zonder viewport, is het resultaat ongedefinieerd. Sommige renderers stempelen de header bij elke scrollstap die ze anders hadden gemaakt, waardoor die elke 600 tot 1000 pixels herhaalt. Anderen vergrendelen hem op één willekeurige y-coördinaat en laten het daarbij. position: sticky loopt tegen hetzelfde probleem aan vanuit de andere richting: de sticky-grens is gedefinieerd ten opzichte van een scrollend bovenliggend element, en er is geen scrollend bovenliggend element als de pagina als één image rendert.

In beide gevallen is de visuele uitvoer fout, en de klikken (die door de in-browser tracker tegen echte paginacoördinaten zijn vastgelegd) worden geplot bovenop headerpixels die niet op de juiste plek staan.

Wat er feitelijk misgaat

Een paar patronen waar we steeds tegenaan lopen:

  • De hoofdsite-header is position: fixed, en de renderer stempelt die elke 600 tot 1000 pixels naar beneden. De hero, de waardepropositie, de testimonials — elk van hen heeft dezelfde nav zwevend over de bovenkant.
  • Een cookiebalk is position: fixed aan de onderkant van de viewport. Die verschijnt halverwege de screenshot en bedekt de prijstabel of het productgrid.
  • Een "terug naar boven"-knop of zwevende chatwidget is position: fixed rechtsonder. Die verschijnt over elke sectie gestempeld, en strooit klikmarkers over content die niets met de knop te maken heeft.
  • Een sub-nav gebruikt position: sticky met een hoge z-index en wordt door de renderer op één willekeurige scrollpositie vergrendeld. Elke klik onder die y-coördinaat, waar ook op de pagina, wordt geplot bovenop de sub-nav.
  • Een modal of banner die de bezoeker had gesloten voordat ze iets aanklikten, verschijnt toch in de screenshot, omdat de gesloten-staat in JavaScript leefde en de renderer opnieuw begon vanuit de geserialiseerde DOM.

Als je heatmap herhaalde nav toont, bevroren overlays, of klikcoördinaten geplot bovenop headerpixels, heb je te maken met minstens één van deze situaties. Op elke site met zowel een hoofdheader als een cookiebalk heb je er twee.

Dit is ook dezelfde familie van problemen die lettertypen, afbeeldingen en scrollcontainers in hetzelfde screenshot breekt. We hebben een uitgebreider artikel over kapotte Matomo heatmap-screenshots geschreven dat de rest behandelt, plus aparte artikelen over lettertypen en afbeeldingen omdat die bijna even vaak voorkomen.

Wat we zelf zouden doen

Als je heatmaps gaat vastleggen op dezelfde pagina's vaker dan een paar keer, ship de ?matomo_heatmap=1 CSS-branch. Het is een paar regels CSS, je stelt het eenmalig in, en de workflow daarna is gewoon een query-parameter. De lichte layoutverschuiving door het weglaten van placeholders is bijna altijd prima.

Als je geen controle hebt over de templates of de CSS, plak je het loskoppel-snippet vóór elke capture. Niet fraai, maar het werkt op elke pagina waar je DevTools op kunt openen.

Voor klantwerk waarbij we heatmaps vastleggen over veel pagina's en geen code in iemand anders zijn stack willen shippen, doet de Matomo Heatmap Helper Chrome-extensie dit automatisch. Die detecteert elke fixed en sticky header, zet ze om naar position: relative met een placeholder van dezelfde hoogte, voert de capture uit en zet alles daarna terug. Dezelfde logica dekt lettertypen, afbeeldingen en scrollcontainers in één keer af. Gratis, open source, code op GitHub.

Martez is het grotere project waar de extensie uit voortgekomen is. Het verbindt Matomo met Meta Ads en Google Ads zodat ROAS, CLV en attributie naast je webanalytics staan in plaats van in een apart spreadsheet. Het zit in private beta. Schrijf je in voor de waitlist als dat relevant voor je is.

Dit is de meeste gevallen één CSS-branch verwijderd. De moeite waard om één keer te doen.

Waarom je sticky header herhaalt (of bevriest) in je Matomo heatmap (en hoe je dat oplost) - Martez Blog