Perché il tuo header sticky si ripete (o si blocca) nella heatmap di Matomo (e come risolvere)

Gli header fissi e sticky si ripetono lungo la pagina o si bloccano nel mezzo degli screenshot delle heatmap di Matomo, perché Matomo rasterizza il DOM serializzato come un'unica immagine alta senza una viewport coerente a cui ancorarsi. Ecco perché succede e come lo gestiamo.

Se hai aperto una heatmap di Matomo e hai trovato il tuo header stampato lungo la pagina due, tre, a volte sei volte come un timbro postale, oppure una copia bloccata in mezzo alla pagina che copre il contenuto sottostante, i click in sé sono a posto. Matomo li ha registrati alle coordinate giuste. Quello che è rotto è lo screenshot che ci sta sotto. Il tuo header sticky non è stato renderizzato come farebbe un browser, quindi si ripete ad ogni posizione di scroll o si ancora dove capitava che il renderer stesse guardando. In ogni caso, ogni click sotto di lui finisce tracciato sopra i link di navigazione, e un bel pezzo della tua heatmap reale diventa un'enorme zona morta.

La soluzione più veloce è un'estensione gratuita per Chrome che manteniamo, chiamata Matomo Heatmap Helper. Rileva ogni header fisso e sticky nella pagina, li converte in position: relative con un placeholder della stessa altezza durante la cattura, e li ripristina dopo. Il resto di questo articolo spiega come fare la stessa cosa senza estensione, più un pattern CSS permanente che vale la pena mettere in produzione se catturi heatmap regolarmente.

Come risolvere senza estensione

C'è uno snippet da console che copre il problema poco prima di ogni cattura, e una branch CSS permanente che puoi mettere in produzione. Scegli quello che si adatta meglio ai tuoi vincoli.

Trova ogni header fisso e sticky nella pagina

Prima di cambiare qualcosa, scopri cosa è effettivamente ancorato. Apri la pagina in Chrome, premi F12 per aprire i DevTools, passa alla Console e incolla:

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));

Qualsiasi cosa riporti fixed o sticky è un candidato. Sulla maggior parte dei siti marketing ne trovi più di uno. L'header principale è fixed, la cookie bar è fixed, un pulsante "torna su" è fixed, un sub-nav secondario è sticky. Si accumulano tutti e rompono lo screenshot ciascuno a modo suo.

Rimuovi lo sticky da ogni header poco prima della cattura

Incolla questo prima di avviare la cattura heatmap. Replica quello che fa il modulo sticky-header-fixer dell'estensione: converte ogni header fisso e sticky in position: relative, e per gli header fixed (che erano staccati dal flusso del documento) inserisce un placeholder invisibile della stessa altezza in modo che la posizione di scroll e le coordinate dei click non si spostino.

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.');
})();

Eseguilo, guarda il conteggio, avvia la cattura. Se restituisce Unstuck 0 headers e vedi comunque un header ripetuto nella pagina, il selettore di cui sopra non copre la classe che usa il tuo header. Ispeziona l'elemento, trova l'elemento contenitore con position: fixed, e aggiungine il selettore a SELECTOR.

Oppure nascondi l'header completamente durante la cattura

Una heatmap su un header sticky raramente dice qualcosa di utile comunque. I visitatori scrollano, l'header si muove con loro, e ogni click sulla nav atterra ad una coordinata Y di pagina diversa. Il risultato è una heatmap della nav spalmata su tutta l'altezza della pagina. Se l'header non è ciò che vuoi analizzare, la mossa più semplice è toglierlo dal quadro completamente:

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

La cattura torna come se l'header non ci fosse. I click reali sotto di lui atterrano comunque sugli elementi giusti, perché le coordinate non cambiano. Perdi solo i dati heatmap (per lo più inutili) sulla nav stessa.

Fix CSS permanente con scope alla modalità di cattura

Se catturi heatmap sulle stesse pagine ripetutamente, il flusso copia-incolla da console diventa noioso. Meglio una branch CSS che si attiva solo quando visiti l'URL con un flag tipo ?matomo_heatmap=1. Aggiungi una classe a <html> quando il flag è presente:

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

Poi metti in produzione del CSS che rimuove lo sticky da ogni header all'interno di quella classe:

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;
}

A quel punto il flusso di cattura diventa: visita ?matomo_heatmap=1, avvia la heatmap, fatto. Niente copia-incolla da console, nessun rischio di dimenticarsi. Il lato negativo è che il trucco del placeholder (che preserva il layout originale per gli header position: fixed) è difficile da esprimere in puro CSS, quindi la pagina potrebbe spostarsi leggermente quando aggiungi la classe. Per la maggior parte dei siti va bene. I dati di click sono quelli che contano, e il layout visivo rimane abbastanza vicino a quello reale da essere leggibile.

Perché Matomo non riesce a renderizzare correttamente gli header sticky

Il renderer delle heatmap di Matomo prende il DOM serializzato e lo rasterizza come un'unica immagine alta, dall'inizio di <body> fino in fondo alla pagina. In quel processo non c'è scroll. Non c'è una viewport nel senso in cui la intende un browser reale. È un'unica grande canvas.

Un header con position: fixed è staccato dal flusso del documento e ancorato a qualunque sia la viewport corrente. Quando il renderer produce un'unica canvas alta senza viewport, il risultato è indefinito. Alcuni renderer stampano l'header ad ogni step di scroll che avrebbero fatto, quindi si ripete ogni 600-1.000 pixel. Altri lo bloccano ad una coordinata y arbitraria e la chiamano buona. position: sticky incappa nello stesso problema dall'altra direzione: il boundary dello sticky è definito rispetto ad un antenato che scrolla, e non c'è nessun antenato che scrolla quando la pagina viene renderizzata come un'unica immagine.

In ogni caso, l'output visivo è sbagliato, e i click (che erano stati registrati rispetto alle coordinate reali della pagina dal tracker nel browser) finiscono tracciati sopra pixel dell'header che non sono nel posto giusto.

Cosa sta fallendo in concreto

Alcuni pattern che incontriamo continuamente:

  • L'header principale del sito è position: fixed, e il renderer lo stampa ogni 600-1.000 pixel lungo la pagina. L'hero, i value prop, le testimonianze: ognuno ha la stessa nav che fluttua in cima.
  • Una cookie bar è position: fixed in fondo alla viewport. Appare a metà dello screenshot, coprendo la tabella dei prezzi o la griglia prodotti.
  • Un pulsante "torna su" o un widget di chat floating è position: fixed in basso a destra. Appare stampato su ogni sezione, con i marcatori di click sparsi su contenuto che non ha nulla a che fare col pulsante.
  • Un sub-nav usa position: sticky con un alto z-index e viene bloccato ad una posizione di scroll arbitraria dal renderer. Ogni click sotto quella coordinata Y, in qualsiasi punto della pagina, finisce tracciato sopra il sub-nav.
  • Una modale o un banner che il visitatore aveva chiuso prima di cliccare su qualcosa appare comunque nello screenshot, perché lo stato di chiusura viveva in JavaScript e il renderer è ripartito da zero dal DOM serializzato.

Se la tua heatmap mostra nav ripetute, overlay bloccati, o coordinate di click tracciate sopra pixel dell'header, stai incappando in almeno uno di questi. Su qualsiasi sito con sia un header principale che una cookie bar, ne stai incappando in due.

Questa è anche la stessa famiglia di problemi che rompe font, immagini e container con scroll nello stesso screenshot. Abbiamo scritto un articolo più lungo sugli screenshot delle heatmap di Matomo rotti che percorre il resto, più articoli separati su font e immagini dato che vengono fuori quasi altrettanto spesso.

Cosa faremmo noi

Se catturi heatmap sulle stesse pagine più di un paio di volte, metti in produzione la branch CSS ?matomo_heatmap=1. Sono poche righe di CSS, la configuri una volta, e il flusso di lavoro da quel momento in poi è solo un query parameter. Il leggero spostamento del layout derivante dal saltare i placeholder va quasi sempre bene.

Se non controlli i template o il CSS, incolla lo snippet di unstick prima di ogni cattura. Non è bello, ma funziona su qualsiasi pagina su cui puoi aprire i DevTools.

Per i lavori con i clienti in cui catturiamo su molte pagine e non vogliamo mettere mano al codice di qualcun altro, l'estensione Chrome Matomo Heatmap Helper lo fa in automatico. Rileva ogni header fisso e sticky, li converte in position: relative con un placeholder della stessa altezza, esegue la cattura e ripristina tutto dopo. La stessa logica copre font, immagini e container con scroll in un colpo solo. Gratuita, open source, codice su GitHub.

Martez è il progetto più grande da cui è nata l'estensione. Connette Matomo con Meta Ads e Google Ads in modo che ROAS, CLV e attribuzione stiano accanto alle tue web analytics invece di stare in un foglio di calcolo separato. È in beta privata. Iscriviti alla waitlist se è rilevante per te.

Il più delle volte, basta una branch CSS. Vale la pena farlo una volta sola.

Perché il tuo header sticky si ripete (o si blocca) nella heatmap di Matomo (e come risolvere) - Martez Blog