Als je een Matomo heatmap hebt geopend en je Cal.com-widget is gerenderd als een 200-pixel-smal stukje met een interne scrollbalk, je YouTube-embed is gekrompen tot een placeholder, en de klikmarkeringen zijn bovenaan het frame opgestapeld in plaats van verspreid over de formuliervelden eronder — dan zijn de klikgegevens er technisch gezien wel. Matomo heeft de coördinaten correct vastgelegd. Wat kapot is, is de iframe in de screenshot eronder. De embed heeft zich nooit uitgevouwen naar zijn contenthoogte toen de Matomo-server de pagina opnieuw renderde, waardoor elk inzicht in formulier-dropoff, kalenderselectie of CTA-klikken binnen de iframe in een klein rechthoekje op zichzelf gestapeld eindigt.
De snelste fix is een gratis Chrome-extensie die wij onderhouden: Matomo Heatmap Helper. Die dwingt elke iframe tijdens het vastleggen op een redelijke hoogte (echte scrollHeight voor same-origin embeds, minimaal 800px voor cross-origin) en zet de oorspronkelijke stijl daarna terug. De rest van dit artikel legt uit hoe je hetzelfde doet zonder extensie, plus de permanente fixes die elke toekomstige capture overleven.
Hoe je het oplost zonder de extensie
Er is een consolefragment dat het probleem oplost vlak voor elke capture, en een aantal permanente fixes die je kunt inbouwen bij de embed. Kies wat past bij de beperkingen van de iframe waarmee je te maken hebt.
Diagnose wat je voor je hebt
Controleer eerst of de iframe same-origin is (je kunt de inhoud lezen) of cross-origin (dat kan niet). Open de pagina in Chrome, druk op F12 om DevTools te openen, klik op de iframe in het Elements-paneel zodat die $0 wordt, en ga dan naar Console:
// Paste in de console na het selecteren van de iframe in het Elements-paneel
console.log('Visible:', $0.getBoundingClientRect().height);
try { console.log('Content:', $0.contentDocument.body.scrollHeight); }
catch (e) { console.log('Content: cross-origin, can\'t read'); }Als de twee getallen niet overeenkomen, is dat het verschil tussen wat de bezoeker zag en wat Matomo vastlegt. Als de tweede regel cross-origin zegt, kan de bovenliggende pagina de inhoud van de embed niet lezen en slaan de meeste fragmenten hieronder die iframe over. Zet in dat geval een minimumhoogte.
Same-origin iframes verkleinen vanuit de console
Als je iframe op dezelfde origin staat als de pagina (een zelf gehost formulier, een interne app ingebed in je marketingsite, een nevenproduct op een subdomein dat je origin deelt via document.domain), kan de bovenliggende pagina de contenthoogte direct uitlezen. Plak dit vlak voor het triggeren van de heatmap-capture:
// Paste in de console. Geeft elke same-origin iframe de hoogte van zijn eigen content.
document.querySelectorAll('iframe').forEach(f => {
try {
const doc = f.contentDocument;
if (doc) f.style.height = doc.body.scrollHeight + 'px';
} catch (e) { /* cross-origin, skip */ }
});Dat is alles voor same-origin. De browser blokkeert contentDocument-toegang op cross-origin iframes, dus vendor-widgets worden stilletjes overgeslagen. Voer daarna het diagnosefragment opnieuw uit om te bevestigen dat de hoogtes kloppen, en trigger dan de capture.
Een minimumhoogte opleggen aan cross-origin iframes
Voor Cal.com, Calendly, YouTube, Stripe Checkout en andere vendor-embeds kan de bovenliggende pagina de inhoud van de iframe niet lezen. Je kunt wel een ruime hoogte van buitenaf instellen — dat is het beste wat je kunt doen zonder medewerking van de ingebedde site:
// Pas MIN_HEIGHT aan naar je hoogste embed (bijv. 800 voor Cal.com, 1200 voor lange formulieren).
const MIN_HEIGHT = 800;
document.querySelectorAll('iframe').forEach(f => {
if (f.getBoundingClientRect().height < MIN_HEIGHT) {
f.style.height = MIN_HEIGHT + 'px';
}
});Dit is een schatting, geen meting. Wees royaal. Een iframe die iets te hoog is rendert lege ruimte onder de content — geen probleem. Een iframe die te laag is, kapt nog steeds de onderkant van het formulier af. Wij gaan standaard op 800 voor boekingswidgets en 1200 voor lange aanmeldformulieren, en passen dat per pagina aan als een specifieke embed meer nodig heeft.
Permanente fix als je beide kanten beheert: postMessage
Als de iframe op een domein staat dat jij beheert (je eigen subdomein, een nevenproduct, een intern tool), is de schoonste oplossing een kleine postMessage-handshake. De kindpagina rapporteert zijn hoogte elke keer dat de content herschikt, de bovenliggende pagina luistert en past toe. Geen gezweef meer.
In de ingebedde kindpagina:
// Voeg toe aan de kindpagina van de iframe
const sendHeight = () => parent.postMessage(
{ type: 'iframe-height', height: document.body.scrollHeight },
'*'
);
new ResizeObserver(sendHeight).observe(document.body);In de bovenliggende pagina:
// Voeg toe aan de bovenliggende pagina
window.addEventListener('message', e => {
if (e.data?.type !== 'iframe-height') return;
document.querySelectorAll('iframe').forEach(f => {
if (f.contentWindow === e.source) f.style.height = e.data.height + 'px';
});
});Dit overleeft lazy-loaded content, lettertypeswaps en dynamische formulierstappen. Het geeft je echte bezoekers ook een soepelere ervaring, want de iframe heeft geen eigen scrollbalk meer nodig.
Gebruik het officiële embed-fragment van de vendor
Cal.com, Calendly en HubSpot leveren allemaal een ingebouwd postMessage-resize-protocol in hun officiële embed-fragmenten. Als je een kale <iframe src="..."> hebt gekopieerd van een docs-pagina of een supportthread, vervang die dan door het kant-en-klare script van de vendor. Die hebben dit al voor je opgelost.
Voor Cal.com is dat het @calcom/embed-snippet-pakket of de <script>-tag uit hun embed builder. Voor Calendly is het het inline embed-fragment op calendly.com. Voor HubSpot is het de formulier-embedcode van het "Share"-tabblad van het formulier. Kale iframes die verwijzen naar de gerenderde URL zijn het patroon dat hier steeds misgaat, en het is bijna altijd een copy-paste-fout in plaats van een bewuste keuze.
iframe-resizer voor willekeurige cross-origin embeds die jij beheert
Als je beide kanten beheert maar het postMessage-protocol niet zelf wil schrijven, neemt het iframe-resizer npm-pakket de randgevallen over (lazy content, laat ladende lettertypen, iOS-eigenaardigheden). Twee scripts, één in de bovenliggende, één in de kindpagina. Zelfde effect als het fragment hierboven, maar met meer battle-testing in browsers.
Vervang de iframe waar mogelijk
Voor YouTube en Vimeo op heatmap-gevolgde pagina's: vervang de iframe door een posterafbeelding en voeg de iframe pas in bij een klik. De bezoeker krijgt de video nog steeds, en de heatmap legt een schoon, volgroots klikdoel vast in plaats van een kleine placeholder. lite-youtube-embed is de canonieke versie van dit patroon, en het is ook sneller voor echte gebruikers omdat de zware YouTube-player alleen laadt als iemand die echt wil.
Dit helpt niet voor boekingswidgets of betaaliframes, waarbij de embed de interactie ís. Het helpt wel voor video, sociale tijdlijnen en alles waarbij de iframe decoratief is totdat erop wordt geklikt.
Waarom Matomo je iframes niet kan verkleinen
Dezelfde onderliggende oorzaak als de lettertype- en afbeeldingsversies van dit probleem, met één extra complicatie. Matomo's screenshot-capture werkt in twee stappen. Eerst serialiseert de tracker je live DOM naar HTML en stuurt die weg. Dan rendert je Matomo-server die HTML tot de screenshot die je in de heatmapweergave ziet. Geen browsersessie, geen cookies, geen Referer die naar je domein verwijst. Gewoon een HTML-bestand dat koud wordt gerenderd vanaf een ander IP-adres.
Bij iframes legt die herrender vast wat de embed toont in zijn standaardstaat. Een iframe is een apart document. De browser blokkeert de bovenliggende pagina om een cross-origin iframe automatisch te verkleinen, dus de hoogte die je in productie ziet is wat de embed-code van de vendor via postMessage heeft onderhandeld bij runtime, plus de CSS-hoogte die je team voor live UX heeft ingesteld. De Matomo-renderer draait JavaScript niet lang genoeg zodat die handshakes landen. Same-origin iframes kunnen technisch gezien worden gemeten, maar de meeste teams vertrouwen op een CSS-hoogte ingesteld voor live UX en niet voor een statische screenshot — waardoor de renderer hoe dan ook eindigt met een standaard ingeklapt frame.
Wat er daadwerkelijk misgaat
Een paar patronen die we steeds tegenkomen:
- Kale
<iframe src="https://cal.com/...">tags in een CMS geplakt in plaats van Cal.com's officiële embed-script. Het officiële script koppelt postMessage. De kale iframe niet. - Calendly inline embeds waarbij de hoogte nergens in CSS is ingesteld. Calendly's eigen fragment stelt die in. Een handgemaakte iframe niet.
- HubSpot- en Marketo-formulieren die asynchroon laden nadat de pagina interactief is. Het formulier kan 600px hoog zijn in productie en 0px hoog op het moment van capture, omdat het JavaScript van het formulier de velden nog niet had geïnjecteerd toen Matomo de DOM serialiseerde.
- YouTube- en Vimeo-iframes op hun standaard 16:9-verhouding voor een 560-pixel-brede kolom die door Matomo bij een andere viewport wordt gerenderd, en zo worden samengedrukt.
- Stripe Checkout en andere betaaliframes die bewust vergrendeld zijn op een klein standaardframe en weigeren uit te vouwen om veiligheidsredenen. De ingebedde vendor bepaalt de hoogte. Jij niet.
- Same-origin iframes met
height: autoingesteld in CSS, wat de browser correct berekent bij runtime maar wat de statische renderserver als 0 behandelt omdat die de layout-pass niet lang genoeg uitvoert.
Als je iframe ingeklapt is in de heatmap, raak je minstens één van deze punten. Formulier-embeds raken er meestal twee.
Dit is dezelfde categorie problemen die lettertypen, afbeeldingen, scrollcontainers en sticky headers kapotmaakt in dezelfde screenshot. We hebben een uitgebreidere post over kapotte Matomo heatmap-screenshots die de rest behandelt, plus aparte posts over waarom lettertypen niet laden en waarom afbeeldingen niet laden — die komen bijna even vaak voor.
Wat we zelf zouden doen
Als de iframe op je eigen domein staat, bouw dan de postMessage-handshake in. Het repareert de heatmap, maakt de live embed soepeler, en je hoeft nooit meer te vechten met layout-verschuivingen.
Als het een vendor-embed is, gebruik dan het officiële fragment van de vendor. Cal.com, Calendly en HubSpot leveren een werkend resize-protocol in hun eigen fragment. Het meeste wat we zien kapotgaan zijn mensen die de gerenderde iframe-URL plakken in plaats van het embed-script.
Als geen van beide haalbaar is (chat van derden, betaaliframes, vendor-boekingswidgets die je de embed niet kunt aanpassen), is de Chrome-extensie Matomo Heatmap Helper wat wij op clientsites gebruiken waar we de stack niet kunnen wijzigen. Die dwingt een verstandige hoogte af tijdens de capture (echte scrollHeight voor same-origin, minimaal 800px voor cross-origin) en zet de oorspronkelijke stijl daarna terug, zodat de live pagina onaangetast blijft. Gratis, open source, code op GitHub.
Martez is het grotere project waar de extensie uit voort is gekomen. Het koppelt Matomo aan Meta Ads en Google Ads zodat ROAS, CLV en attributie naast je webanalytics staan in plaats van in een apart spreadsheet. Het is in private beta. Meld je aan voor de wachtlijst als dat relevant voor je is.
Meestal is dit een vendor-snippet-swap of een postMessage-handshake verwijderd. De moeite waard om één keer in te bouwen.