Se você abriu um heatmap do Matomo e encontrou ícones de imagem quebrada onde o logo e o hero deveriam estar, imagens de fundo sumindo em cada seção, e nos casos mais graves um layout que parece que a página esqueceu que tem CSS — os cliques em si estão certos. O Matomo registrou tudo nas coordenadas corretas. O que quebrou foi a captura de tela embaixo deles. Qualquer coisa na página com um caminho relativo não sobreviveu à viagem até o renderizador do Matomo, e você fica com um heatmap flutuando sobre o wireframe do que seus visitantes realmente viram.
O sintoma é bem consistente. Qualquer coisa com uma URL completa como https://yoursite.com/images/hero.jpg renderiza normal. Qualquer coisa com um caminho relativo (/images/hero.jpg, ./logo.svg, assets/bg.png) está quebrada. O mesmo vale para stylesheets, fontes, scripts e referências url(...) do CSS dentro de estilos inline. Assim que você percebe esse padrão, a causa fica óbvia.
A correção mais rápida é uma extensão gratuita pra Chrome que a gente mantém chamada Matomo Heatmap Helper. Ela percorre o DOM e converte cada URL relativa pra absoluta logo antes de cada captura, depois restaura as originais. O restante deste post mostra como fazer a mesma coisa sem extensão, mais algumas correções permanentes que valem a pena subir pra produção.
Como corrigir sem a extensão
Tem um snippet de console que resolve o problema bem antes de cada captura, e algumas correções permanentes que você pode subir junto com o site ou no build. Escolha o que se encaixa melhor nas suas restrições.
Liste todas as URLs relativas da página
Antes de mudar qualquer coisa, descubra o que é relativo de verdade. Abra a página no Chrome, aperte F12 pra abrir o DevTools, vá pra aba Console e cole isso. O snippet cobre <img src>, <img srcset>, <source srcset>, <source src>, <video poster> e background-image em estilos inline:
// Paste into the browser console. Lists every relative resource URL.
(() => {
const isRel = u => u && !/^([a-z]+:|\/\/|data:|blob:|#)/i.test(u.trim());
const out = [];
document.querySelectorAll('img[src]').forEach(el =>
isRel(el.getAttribute('src')) && out.push(['img.src', el.getAttribute('src'), el]));
document.querySelectorAll('img[srcset], source[srcset]').forEach(el =>
el.getAttribute('srcset').split(',').map(s => s.trim().split(/\s+/)[0])
.forEach(u => isRel(u) && out.push(['srcset', u, el])));
document.querySelectorAll('video[poster]').forEach(el =>
isRel(el.getAttribute('poster')) && out.push(['video.poster', el.getAttribute('poster'), el]));
document.querySelectorAll('source[src]').forEach(el =>
isRel(el.getAttribute('src')) && out.push(['source.src', el.getAttribute('src'), el]));
document.querySelectorAll('[style*="url("]').forEach(el => {
const m = el.getAttribute('style').match(/url\(\s*['"]?([^'")]+)['"]?\s*\)/g) || [];
m.forEach(raw => {
const u = raw.match(/url\(\s*['"]?([^'")]+)/)[1];
if (isRel(u)) out.push(['inline bg', u, el]);
});
});
console.table(out.map(([t, u]) => ({ type: t, url: u })));
return out;
})();Isso te dá a lista exata de referências que o renderizador do Matomo vai resolver de forma errada. Se a tabela voltar vazia e o heatmap ainda estiver quebrado, o problema está em outro lugar (CORS num CDN, licenciamento de fonte, containers com scroll). Caso contrário, cada linha da tabela é algo que você precisa reescrever ou mover pra uma URL absoluta.
A correção rápida: reescrever URLs relativas pra absolutas antes da captura
Esse é o snippet que a gente cola no console do navegador logo antes de acionar a captura de heatmap do Matomo. Ele espelha o relative-url-fixer da extensão: percorre o DOM, resolve cada URL relativa de recurso em relação à localização atual da página e grava a versão absoluta de volta no atributo. Na hora que o Matomo serializa o DOM, toda referência já está completamente qualificada e o renderizador busca os assets da sua origem em vez da do Matomo. Ele deixa <a href> de lado de propósito, já que reescrever links de navegação quebraria o roteamento de SPA sem afetar a captura de tela.
// Paste into the browser console. Converts every relative resource URL to
// absolute so Matomo's server-side render resolves them against your origin
// instead of Matomo's. Skips <a href> deliberately, since changing nav links
// would break SPAs and doesn't affect the screenshot.
(() => {
const base = location.href;
const isRel = u => u && !/^([a-z]+:|\/\/|data:|blob:|#)/i.test(u.trim());
const abs = u => { try { return new URL(u, base).href; } catch { return u; } };
let count = 0;
// 1. img[src], source[src], script[src], video[poster], stylesheets, icons, preloads
document.querySelectorAll('img[src], source[src], script[src]').forEach(el => {
const v = el.getAttribute('src');
if (isRel(v)) { el.setAttribute('src', abs(v)); count++; }
});
document.querySelectorAll('video[poster]').forEach(el => {
const v = el.getAttribute('poster');
if (isRel(v)) { el.setAttribute('poster', abs(v)); count++; }
});
document.querySelectorAll('link[rel="stylesheet"], link[rel~="icon"], link[rel="preload"]').forEach(el => {
const v = el.getAttribute('href');
if (isRel(v)) { el.setAttribute('href', abs(v)); count++; }
});
// 2. img[srcset], source[srcset]. Rewrite each candidate and preserve descriptors.
document.querySelectorAll('img[srcset], source[srcset]').forEach(el => {
const srcset = el.getAttribute('srcset');
const rewritten = srcset.split(',').map(part => {
const trimmed = part.trim();
const [u, ...rest] = trimmed.split(/\s+/);
return isRel(u) ? [abs(u), ...rest].join(' ') : trimmed;
}).join(', ');
if (rewritten !== srcset) { el.setAttribute('srcset', rewritten); count++; }
});
// 3. CSS url() inside inline styles
document.querySelectorAll('[style*="url("]').forEach(el => {
const before = el.getAttribute('style');
const after = before.replace(
/url\(\s*['"]?([^'")]+)['"]?\s*\)/g,
(whole, u) => isRel(u) ? `url("${abs(u)}")` : whole
);
if (after !== before) { el.setAttribute('style', after); count++; }
});
console.log(`Rewrote ${count} relative URLs to absolute. Now trigger your Matomo heatmap capture.`);
})();Quando a linha de log aparecer, faça a captura imediatamente. A reescrita só dura até a próxima navegação. Note que esse snippet enxerga apenas atributos style="..." inline, não chamadas url(...) dentro de stylesheets externos. Para esses, veja o reescritor pós-build mais abaixo, ou use a correção no momento do build.
Adicione uma tag <base> no <head>
A correção permanente mais simples possível. Uma linha no topo do <head> e cada URL relativa da página passa a resolver em relação ao seu domínio em vez do host que estiver renderizando o HTML:
<!-- Edit yoursite.com to your domain -->
<base href="https://yoursite.com/">O porém é que <base> afeta todas as URLs relativas do site, incluindo links de navegação. SPAs que dependem de roteamento relativo (React Router, <Link> do Next.js em algumas configurações, qualquer coisa com pushState no lado cliente usando caminhos relativos) podem se comportar mal depois de adicionar isso. Teste o fluxo de navegação antes de subir pra produção, ou limite a tag pra que ela só apareça durante as capturas de heatmap (um parâmetro de query que o script de rastreamento ativa, por exemplo).
Se você consegue adicionar uma tag <base> sem quebrar a navegação do SPA, essa é a resposta e você pode parar de ler.
Emita URLs absolutas a partir do build
Se <base> não é uma opção, configure seu build pra emitir URLs absolutas pra assets desde o início. A maioria dos geradores tem uma configuração pra isso. Next.js tem assetPrefix no next.config.js. Hugo tem baseURL no config.toml. Astro tem site no astro.config.mjs. Gatsby usa pathPrefix junto com --prefix-paths no momento do build. Defina o valor pro seu domínio de produção, faça o rebuild, e cada tag <img>, <link> e <script> que o gerador emitir vai sair completamente qualificada. O renderizador do Matomo busca a partir da sua origem e o heatmap funciona.
O problema aqui é que URLs absolutas geradas no build normalmente cobrem só os assets que o framework conhece. CSS escrito à mão (ou vindo de um CMS, ou de um tema customizado) costuma escapar, e também as referências url(...) dentro de stylesheets compilados. Se você configurou o build corretamente e o heatmap ainda tem fundos ou fontes de ícone faltando, é esse gap que você está enfrentando.
Reescritor pós-build para url() no CSS
Se o seu gerador deixa passar url(...) dentro de stylesheets compilados, esse é um script Node pequeno que percorre o diretório de saída e reescreve cada caminho root-relativo pra um absoluto:
// Edit SITE to your domain. Run with: node rewrite.js
import { readFileSync, writeFileSync } from 'fs';
import { globSync } from 'glob';
const SITE = 'https://yoursite.com';
for (const f of globSync('out/**/*.{html,css}')) {
const out = readFileSync(f, 'utf8').replace(
/url\(\s*['"]?(\/[^'")]+)['"]?\s*\)/g,
(_, p) => `url(${SITE}${p})`
);
writeFileSync(f, out);
}Conecte-o ao seu CI como uma etapa pós-build. Ele é intencionalmente limitado a caminhos root-relativos (caminhos que começam com /), já que reescrever ./foo ou ../bar exige saber onde o stylesheet está na árvore de saída. Se você tem muitos desses, a correção no momento do build acima é a melhor resposta.
Por que o Matomo não consegue resolver URLs relativas
A captura de tela do Matomo é um processo em duas etapas. Primeiro, o tracker serializa seu DOM ativo em HTML e envia. Depois, seu servidor Matomo re-renderiza esse HTML pra produzir a captura que você vê na visualização do heatmap. O HTML ainda contém /images/hero.jpg exatamente como você escreveu. Mas o renderizador não está rodando no seu domínio, então quando o motor do navegador tenta carregar essa imagem, ele resolve o caminho relativo em relação ao host do Matomo. /images/hero.jpg vira https://seu-host-matomo/images/hero.jpg, que não existe. O mesmo vale para stylesheets, fontes e as chamadas url(...) dentro de estilos inline. O renderizador pede ao servidor do Matomo por arquivos que ele nunca viu, recebe 404s de volta, e você fica com uma captura em que tudo com caminho relativo está faltando.
É também por isso que URLs completamente qualificadas funcionam normal. Uma vez que uma URL tem seu próprio scheme e host, o renderizador sabe exatamente onde buscar e a requisição vai pra sua origem como deveria.
O que está falhando de verdade
Alguns padrões que a gente continua encontrando:
<img src="/images/hero.jpg">e similares. O sintoma mais visível: ícones de imagem quebrada onde deveriam estar o hero, o logo ou as fotos de produto.<link rel="stylesheet" href="/css/main.css">comhrefrelativo. O stylesheet inteiro retorna 404, então a página renderiza com os padrões do navegador. Esse é o caso que as pessoas descrevem como "o heatmap parece completamente sem estilo."style="background-image: url(/images/bg.png)"inline. Os fundos somem, o layout parece OK mas está visivelmente quebrado.- Candidatos de
srcsetcom caminhos relativos dentro de<img srcset>ou<source srcset>. Qual candidato o renderizador do Matomo escolhe depende da suposição de viewport dele, então às vezes um caminho resolve e outro não, e a quebra parece intermitente. @font-face src: url("/fonts/inter.woff2")declarado dentro de um bloco<style>inline ou de um stylesheet que sobrevive. A fonte retorna 404 e você cai de volta pras fontes do sistema. (Isso se sobrepõe ao post sobre fontes que não carregam, mas aqui a causa é resolução de caminho, não CORS.)<use href="/icons/sprite.svg#chevron">no SVG. O sprite nunca carrega, cada ícone some e o layout colapsa em torno dos placeholders<svg>vazios.<link rel="preload" as="font" href="...">comhrefrelativo. Não quebra a captura por si só, mas é um sinal útil de que outros assets da mesma família provavelmente também estão quebrados.
Se o seu heatmap está mostrando placeholders de imagem quebrada, CSS faltando ou um layout que não bate com o seu site ao vivo, você está enfrentando pelo menos um desses. Com frequência mais de um na mesma página.
Essa também é a mesma família de problema que quebra fontes, imagens, containers com scroll e headers sticky na mesma captura. A gente escreveu um post mais completo sobre capturas de tela quebradas do heatmap do Matomo que aborda o restante, além de posts separados sobre por que as fontes não carregam e por que as imagens não carregam, já que cada um tem suas próprias causas.
O que a gente faria na prática
Se você consegue adicionar uma tag <base> sem quebrar a navegação do SPA, essa é a resposta. Uma linha, cada URL relativa resolve corretamente, pronto.
Se <base> conflita com seu roteamento, configure o build pra emitir URLs absolutas pra assets e adicione o reescritor CSS pós-build pras referências url(...) que o build não alcança. Um pouco mais de trabalho, mais completo, não briga com o framework.
Se nenhuma das opções é viável (templates gerenciados por CMS que você não pode tocar, temas que não são seus, markup injetado por terceiros), a extensão Matomo Heatmap Helper pra Chrome é o que a gente usa em sites de clientes onde não consegue mudar a stack. Ela roda a mesma lógica de reescrita do snippet acima, mais correções equivalentes pra fontes, imagens, containers com scroll e headers sticky, em cada captura. Gratuita, open source, código no GitHub.
O Martez é o projeto maior do qual a extensão nasceu. 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 em uma planilha separada. Está em beta privado. Entre na lista de espera se isso for relevante pra você.
A maioria dos problemas de URL relativa é uma configuração de build. Vale fazer uma vez.