Por que o seu header sticky se repete (ou trava) no heatmap do Matomo (e como corrigir)

Headers fixos e sticky se repetem ao longo da página ou travam no meio das capturas de tela dos heatmaps do Matomo, porque o Matomo renderiza o DOM capturado como uma imagem alta única sem viewport consistente pra eles se ancorarem. Veja por que isso acontece e como lidamos com isso.

Se você já abriu um heatmap do Matomo e encontrou seu header carimbado página abaixo duas, três, às vezes seis vezes como um carimbo de correios, ou uma cópia dele travada no meio da tela cobrindo o conteúdo por baixo, os cliques em si estão certos. O Matomo os registrou nas coordenadas corretas. O que está quebrado é a captura de tela por baixo deles. Seu header sticky não renderizou como renderiza num navegador, então ele ou se repete em cada posição de scroll ou se fixa em qualquer lugar que o renderizador estava olhando. De qualquer forma, todo clique por baixo dele fica plotado em cima de links de nav, e um pedaço do seu heatmap real vira uma grande zona morta.

A correção mais rápida é uma extensão gratuita pra Chrome que mantemos chamada Matomo Heatmap Helper. Ela detecta cada header fixed e sticky na página, troca para position: relative com um placeholder de mesma altura durante a captura, e restaura tudo depois. O resto deste post é como fazer a mesma coisa sem extensão, mais um padrão CSS permanente que vale a pena usar se você vai capturar heatmaps com frequência.

Como corrigir sem a extensão

Tem um snippet de console que resolve o problema logo antes de cada captura, e um branch de CSS permanente que você pode subir junto com o site. Escolha o que melhor se encaixa nas suas restrições.

Encontre cada header fixed e sticky na página

Antes de mudar qualquer coisa, descubra o que está ancorado de verdade. Abra a página no Chrome, aperte F12 pra abrir o DevTools, vá pra aba Console, e cole:

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

Qualquer coisa reportando fixed ou sticky é suspeita. Na maioria dos sites de marketing você vai encontrar mais de um. O header principal é fixed, a barra de cookies é fixed, um botão "voltar ao topo" é fixed, um sub-nav secundário é sticky. Todos se acumulam e todos quebram a captura de tela de formas diferentes.

Desgrude cada header logo antes da captura

Cole isso antes de acionar a captura do heatmap. Isso espelha o que o sticky-header-fixer da extensão faz: converte cada header fixed e sticky pra position: relative, e pra headers fixed (que estavam fora do fluxo do documento) insere um placeholder invisível de mesma altura pra que a posição de scroll e as coordenadas de clique não mudem.

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

Rode, veja a contagem, acione a captura. Se logar Unstuck 0 headers e você ainda consegue ver um header se repetindo na página, a lista de seletores acima está faltando a classe que seu header usa. Inspecione o elemento, encontre o elemento que tem position: fixed, e adicione o seletor dele no SELECTOR.

Ou simplesmente esconda o header durante a captura

Um heatmap num header sticky raramente te diz alguma coisa útil de qualquer forma. Os visitantes rolam a página, o header se move junto com eles, e cada clique na nav cai numa coordenada de página Y diferente. O resultado é um heatmap da nav espalhado pela altura inteira da página. Se o header não é o que você quer aprender, o caminho mais simples é tirá-lo completamente da imagem:

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

A captura volta como se o header não existisse. Cliques reais por baixo ainda caem nos elementos certos, porque as coordenadas de clique não mudam. Você só perde o dado de heatmap na nav em si (que seria inútil mesmo).

Correção CSS permanente com escopo no modo de captura

Se você vai capturar heatmaps nas mesmas páginas repetidamente, o fluxo de colar no console cansa. Melhor é um branch de CSS que ativa somente quando você visita a URL com um flag como ?matomo_heatmap=1. Adicione uma classe ao <html> quando o flag estiver presente:

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

Depois suba o CSS que desgruda cada header dentro dessa 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í o fluxo de captura fica assim: visite ?matomo_heatmap=1, acione o heatmap, pronto. Sem colar no console, sem risco de esquecer. A desvantagem é que o truque do placeholder (preservar o layout original pra headers com position: fixed) é mais difícil de expressar em CSS puro, então a página pode deslocar um pouco quando você ativa a classe. Na maioria dos sites isso não é problema. O que importa são os dados de clique, e o visual fica próximo o suficiente do layout real pra ser legível.

Por que o Matomo não consegue renderizar headers sticky direito

O renderizador de heatmaps do Matomo pega o DOM serializado e o rasteriza como uma única imagem alta, com a altura total da página do topo do <body> até o final. Não tem scroll nesse pipeline. Não tem viewport do jeito que um navegador real tem viewport. É uma tela grande só.

Um header com position: fixed está desacoplado do fluxo do documento e fixado em qualquer que seja o viewport atual. Quando o renderizador está produzindo uma tela alta sem viewport, o resultado é indefinido. Alguns renderizadores carimbam o header a cada passo de scroll que teriam feito, então ele se repete a cada 600 a 1.000 pixels. Outros o travam numa coordenada Y arbitrária e pronto. O position: sticky esbarra no mesmo problema pelo outro lado: a fronteira sticky é definida em relação a um ancestral com scroll, e não existe ancestral com scroll quando a página renderiza como uma única imagem.

De qualquer jeito, a saída visual está errada, e os cliques (que foram registrados contra as coordenadas reais da página pelo tracker no navegador) acabam plotados em cima de pixels do header que não estão no lugar certo.

O que está falhando de verdade

Alguns padrões que continuamos encontrando:

  • O header principal do site tem position: fixed, e o renderizador o carimba a cada 600 a 1.000 pixels abaixo na página. O hero, os value props, os depoimentos, cada um deles tem a mesma nav flutuando no topo.
  • Uma barra de consentimento de cookies tem position: fixed na parte inferior do viewport. Ela aparece no meio da captura, cobrindo a tabela de preços ou a grade de produtos.
  • Um botão "voltar ao topo" ou widget de chat flutuante tem position: fixed no canto inferior direito. Ele aparece carimbado sobre cada seção, jogando marcadores de clique em cima de conteúdo que não tem nada a ver com o botão.
  • Um sub-nav usa position: sticky com z-index alto e fica travado numa posição de scroll arbitrária pelo renderizador. Todo clique abaixo dessa coordenada Y, em qualquer lugar da página, fica plotado em cima do sub-nav.
  • Um modal ou banner que o visitante fechou antes de clicar em qualquer coisa ainda aparece na captura, porque o estado de fechar vivia no JavaScript e o renderizador começou do zero a partir do DOM serializado.

Se o seu heatmap está mostrando nav repetida, sobreposições travadas ou coordenadas de clique plotadas em cima de pixels de header, você está batendo em pelo menos um desses. Em qualquer site com um header principal e uma barra de cookies, você está batendo em dois.

Essa é também a mesma família de problema que quebra fontes, imagens e containers com scroll na mesma captura. Escrevemos um post mais longo sobre capturas de tela quebradas do Matomo que percorre o resto, mais posts separados sobre fontes e imagens já que esses aparecem quase com a mesma frequência.

O que faríamos na prática

Se você vai capturar heatmaps nas mesmas páginas mais de algumas vezes, suba o branch de CSS com ?matomo_heatmap=1. São algumas linhas de CSS, você configura uma vez, e o fluxo depois é só um query parameter. O leve deslocamento de layout por pular os placeholders é quase sempre aceitável.

Se você não controla os templates ou o CSS, cole o snippet de desgrude antes de cada captura. Não é elegante, mas funciona em qualquer página que você consiga abrir o DevTools.

Para trabalhos com clientes onde estamos capturando em muitas páginas e não queremos subir código na stack de outra pessoa, a extensão Matomo Heatmap Helper para Chrome faz isso automaticamente. Ela detecta cada header fixed e sticky, troca pra position: relative com um placeholder de mesma altura, roda a captura e restaura tudo depois. A mesma lógica cobre fontes, imagens e containers com scroll de uma vez. Gratuita, open source, código no GitHub.

O Martez é o projeto maior do qual a extensão surgiu. Ele conecta o Matomo com Meta Ads e Google Ads pra que ROAS, CLV e atribuição fiquem ao lado da sua análise web em vez de numa planilha separada. Está em beta privado. Entre na lista de espera se isso for relevante pra você.

Na maioria das vezes, é um branch de CSS de distância. Vale fazer uma vez.

Por que o seu header sticky se repete (ou trava) no heatmap do Matomo (e como corrigir) - Martez Blog