Wenn du eine Matomo-Heatmap geöffnet und dein Cal.com-Widget als 200-Pixel-Streifen mit internem Scrollbalken vorgefunden hast, dein YouTube-Embed auf einen Platzhalter geschrumpft ist und die Klickmarkierungen sich oben im Frame stapeln statt über die Formularfelder darunter verteilt zu sein: Die Klickdaten sind technisch gesehen vorhanden. Matomo hat die Koordinaten korrekt aufgezeichnet. Was kaputt ist, ist das iframe im dahinterliegenden Screenshot. Das Embed hat sich beim erneuten Rendern der Seite durch den Matomo-Server nie auf seine Inhaltshöhe ausgedehnt. Damit landen alle Erkenntnisse über Formular-Absprünge, Kalenderauswahl oder CTA-Klicks innerhalb des iframes aufeinander gestapelt in einem winzigen Rechteck.
Der schnellste Fix ist eine kostenlose Chrome-Erweiterung, die wir pflegen: Matomo Heatmap Helper. Sie zwingt jedes iframe während der Erfassung auf eine vernünftige Höhe (echtes scrollHeight für Same-Origin-Embeds, mindestens 800px für Cross-Origin-Embeds) und stellt danach den ursprünglichen Stil wieder her. Der Rest dieses Posts erklärt, wie du dasselbe ohne Erweiterung hinbekommst, plus die dauerhaften Fixes, die auch künftige Erfassungen überleben.
So behebst du es ohne die Erweiterung
Es gibt ein Konsolen-Snippet, das das Problem kurz vor jeder Erfassung überbrückt, und ein paar dauerhafte Fixes, die du zusammen mit dem Embed ausliefern kannst. Nimm, was zu den Einschränkungen des jeweiligen iframes passt.
Erstmal diagnostizieren
Bevor du irgendetwas änderst, prüfe, ob das iframe Same-Origin ist (du kannst seinen Inhalt lesen) oder Cross-Origin (kannst du nicht). Öffne die Seite in Chrome, drück F12 für DevTools, klick das iframe im Elements-Panel an, damit es zu $0 wird, und wechsel dann zur Konsole:
// Paste in the console after selecting the iframe in the Elements panel
console.log('Visible:', $0.getBoundingClientRect().height);
try { console.log('Content:', $0.contentDocument.body.scrollHeight); }
catch (e) { console.log('Content: cross-origin, can\'t read'); }Wenn die beiden Zahlen nicht übereinstimmen, ist das die Lücke zwischen dem, was der Besucher gesehen hat, und dem, was Matomo erfasst. Wenn die zweite Zeile cross-origin anzeigt, kann die Elternseite den Inhalt des Embeds nicht lesen, und die meisten der folgenden Snippets überspringen es. Erzwing in dem Fall eine Mindesthöhe.
Same-Origin-iframes per Konsole vergrößern
Wenn dein iframe auf demselben Origin wie die Seite liegt (ein selbst gehostetes Formular, eine interne App, die in deine Marketingseite eingebettet ist, ein Schwesterprodukt auf einer Subdomain, das über document.domain deinen Origin teilt), kann die Elternseite die Inhaltshöhe direkt auslesen. Paste das kurz vor dem Auslösen der Heatmap-Erfassung:
// Paste in the console. Resizes every same-origin iframe to its content height.
document.querySelectorAll('iframe').forEach(f => {
try {
const doc = f.contentDocument;
if (doc) f.style.height = doc.body.scrollHeight + 'px';
} catch (e) { /* cross-origin, skip */ }
});Das war's für Same-Origin. Der Browser blockiert den contentDocument-Zugriff auf Cross-Origin-iframes, deshalb wird jedes Vendor-Widget stillschweigend übersprungen. Führ das Diagnose-Snippet nochmal aus, um sicherzustellen, dass die Höhen stimmen, und löse dann die Erfassung aus.
Mindesthöhe für Cross-Origin-iframes erzwingen
Für Cal.com, Calendly, YouTube, Stripe Checkout und alle anderen Vendor-Embeds kann die Elternseite den Inhalt des iframes nicht lesen. Du kannst aber von außen eine ausreichende Höhe setzen – das Beste, was du tun kannst, ohne dass die eingebettete Seite mitmacht:
// Edit MIN_HEIGHT to fit your tallest embed (e.g. 800 for Cal.com, 1200 for long forms).
const MIN_HEIGHT = 800;
document.querySelectorAll('iframe').forEach(f => {
if (f.getBoundingClientRect().height < MIN_HEIGHT) {
f.style.height = MIN_HEIGHT + 'px';
}
});Das ist eine Schätzung, keine Messung. Lieber großzügig. Ein iframe, das etwas zu groß ist, rendert Leerraum unterhalb des Inhalts – kein Problem. Eines, das zu klein ist, schneidet den unteren Teil des Formulars immer noch ab. Wir nehmen standardmäßig 800 für Buchungs-Widgets und 1200 für lange Anmeldeformulare, und passen per Seite an, wenn ein bestimmtes Embed mehr braucht.
Dauerhafter Fix, wenn du beide Seiten kontrollierst: postMessage
Wenn das iframe auf einer Domain liegt, die du kontrollierst (deine eigene Subdomain, ein Schwesterprodukt, ein internes Tool), ist die sauberste Lösung ein kleiner postMessage-Handshake. Die Child-Seite meldet ihre Höhe jedes Mal, wenn der Inhalt neu fließt, die Elternseite hört zu und setzt sie. Kein Raten mehr.
In der eingebetteten Child-Seite:
// Add to the iframe's child page
const sendHeight = () => parent.postMessage(
{ type: 'iframe-height', height: document.body.scrollHeight },
'*'
);
new ResizeObserver(sendHeight).observe(document.body);In der Elternseite:
// Add to the parent page
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';
});
});Das übersteht Lazy-Loaded-Content, Font-Swaps und dynamische Formularschritte. Außerdem verbessert es die Nutzererfahrung, weil das iframe keinen eigenen Scrollbalken mehr braucht.
Das offizielle Embed-Snippet des Anbieters verwenden
Cal.com, Calendly und HubSpot liefern alle ein eingebautes postMessage-Resize-Protokoll in ihren offiziellen Embed-Snippets mit. Wenn du ein blankes <iframe src="..."> von einer Docs-Seite oder einem Support-Thread kopiert hast, tausch es gegen das Drop-in-Skript des Anbieters aus. Die haben das bereits für dich gelöst.
Für Cal.com ist das das @calcom/embed-snippet-Paket oder das <script>-Tag aus ihrem Embed-Builder. Für Calendly ist es das Inline-Embed-Snippet auf calendly.com. Für HubSpot ist es der Formular-Embed-Code aus dem "Teilen"-Tab des Formulars. Blanke iframes, die auf die gerenderte URL zeigen, sind das häufige Muster, das hier kaputt geht, und es ist fast immer ein Copy-Paste-Fehler und keine bewusste Entscheidung.
iframe-resizer für beliebige Cross-Origin-Embeds, die du kontrollierst
Wenn du beide Seiten kontrollierst, aber das postMessage-Protokoll nicht selbst schreiben willst, übernimmt das npm-Paket iframe-resizer die Randfälle (Lazy-Content, spät geladene Schriftarten, iOS-Quirks). Zwei Skripte, eines im Parent, eines im Child. Gleicher Effekt wie das Snippet oben, mit mehr Browser-Kampferprobung.
Das iframe ersetzen, wo es möglich ist
Für YouTube und Vimeo auf heatmap-getrackten Seiten: Tausch das iframe gegen ein Posterbild aus und füge das iframe erst beim Klick ein. Der Besucher bekommt das Video trotzdem, und die Heatmap erfasst ein sauberes, vollständiges Klickziel statt eines winzigen Platzhalters. lite-youtube-embed ist die kanonische Version dieses Musters, und für echte Nutzer ist es auch schneller, weil der schwere YouTube-Player nur lädt, wenn jemand ihn tatsächlich will.
Das hilft nicht bei Buchungs-Widgets oder Zahlungs-iframes, wo das Embed die eigentliche Interaktion ist. Es hilft bei Videos, Social-Timelines und allem anderen, wo das iframe dekorativ ist, bis jemand draufklickt.
Warum Matomo deine iframes nicht selbst skalieren kann
Dieselbe Grundursache wie bei den Font- und Bild-Varianten des Problems, mit einem zusätzlichen Haken. Matomos Screenshot-Erfassung läuft in zwei Schritten ab. Zuerst serialisiert der Tracker dein Live-DOM in HTML und schickt es weg. Dann rendert dein Matomo-Server dieses HTML, um den Screenshot zu erzeugen, den du in der Heatmap-Ansicht siehst. Keine Browser-Session, keine Cookies, kein Referer, der auf deine Domain zeigt. Nur eine HTML-Datei, die kalt von einer anderen IP gerendert wird.
Beim Rendern erfasst der Server, was das Embed in seinem Standardzustand zeigt. Ein iframe ist ein separates Dokument. Der Browser blockiert die Elternseite daran, ein Cross-Origin-iframe automatisch zu skalieren. Die Höhe, die du in der Produktion siehst, ist also das, was der Embed-Code des Anbieters zur Laufzeit per postMessage ausgehandelt hat, plus jede CSS-Höhe, die dein Team für die Live-UX gesetzt hat. Der Matomo-Renderer lässt JavaScript nicht lange genug laufen, damit diese Handshakes abgeschlossen werden. Same-Origin-iframes können technisch gesehen gemessen werden, aber die meisten Teams verlassen sich auf eine für die Live-UX gesetzte CSS-Höhe und nicht auf einen statischen Screenshot – deshalb landet der Renderer auch da mit einem standardmäßig eingeklappten Frame.
Was konkret schiefläuft
Ein paar Muster, auf die wir immer wieder stoßen:
- Blanke
<iframe src="https://cal.com/...">-Tags, die in ein CMS eingefügt wurden statt Cal.coms offiziellem Embed-Skript. Das offizielle Skript verkabelt postMessage. Das blanke iframe nicht. - Calendly-Inline-Embeds, bei denen nirgendwo in CSS eine Höhe gesetzt ist. Calendlys eigenes Snippet setzt sie. Ein von Hand gebautes iframe nicht.
- HubSpot- und Marketo-Formulare, die asynchron laden, nachdem die Seite interaktiv ist. Das Formular kann in der Produktion 600px hoch sein und zum Erfassungszeitpunkt 0px, weil das Formular-JavaScript die Felder noch nicht eingefügt hatte, als Matomo das DOM serialisiert hat.
- YouTube- und Vimeo-iframes im Standard-16:9-Verhältnis für eine 560-Pixel-Spalte, die von Matomo bei einem anderen Viewport gerendert werden und gequetscht enden.
- Stripe Checkout und andere Zahlungs-iframes, die absichtlich auf einen kleinen Standard-Frame gesperrt sind und sich aus Sicherheitsgründen weigern zu expandieren. Der eingebettete Anbieter entscheidet über die Höhe. Du nicht.
- Same-Origin-iframes mit
height: autoin CSS, was der Browser zur Laufzeit korrekt berechnet, was der Server für statisches Rendering aber als 0 behandelt, weil er den Layout-Pass nicht lange genug ausführt.
Wenn dein iframe in der Heatmap eingeklappt ist, trifft mindestens eines davon zu. Formular-Embeds treffen meist zwei.
Das gehört zur gleichen Problemfamilie wie fehlerhafte Schriftarten, Bilder, Scroll-Container und fixierte Header im gleichen Screenshot. Wir haben einen längeren Post über fehlerhafte Matomo-Heatmap-Screenshots, der den Rest durchgeht, plus separate Posts dazu, warum Schriftarten nicht laden und warum Bilder nicht laden, da diese fast genauso häufig auftauchen.
Was wir tatsächlich tun würden
Wenn das iframe auf deiner eigenen Domain liegt, ship den postMessage-Handshake. Er behebt die Heatmap, glättet das Live-Embed und du hörst für immer auf, mit Layout-Verschiebungen zu kämpfen.
Wenn es ein Vendor-Embed ist, nutz das offizielle Snippet des Anbieters. Cal.com, Calendly und HubSpot liefern ein funktionierendes Resize-Protokoll darin mit. Die meisten Fehler, die wir sehen, kommen daher, dass Leute die gerenderte iframe-URL kopiert haben statt des Embed-Skripts.
Wenn keins davon erreichbar ist (Third-Party-Chat, Zahlungs-iframes, Anbieter-Buchungs-Widgets, deren Embed du nicht ändern kannst), ist die Matomo Heatmap Helper-Chrome-Erweiterung das, was wir auf Kundenseiten einsetzen, wo wir den Stack nicht ändern können. Sie erzwingt eine vernünftige Höhe während der Erfassung (echtes scrollHeight für Same-Origin, mindestens 800px für Cross-Origin) und stellt danach den ursprünglichen Stil wieder her, sodass die Live-Seite unberührt bleibt. Kostenlos, Open Source, Code auf GitHub.
Martez ist das größere Projekt, aus dem die Erweiterung entstanden ist. Es verbindet Matomo mit Meta Ads und Google Ads, sodass ROAS, CLV und Attribution direkt neben deiner Web-Analytics liegen statt in einer separaten Tabelle. Es befindet sich in der Private Beta. Trag dich auf die Warteliste ein, wenn das für dich relevant ist.
Meistens ist es ein Vendor-Snippet-Swap oder ein postMessage-Handshake entfernt. Den Aufwand wert, es einmal richtig zu verdrahten.