Matomoヒートマップで相対URLが読み込まれない原因と修正方法

ページが相対パスを使っているとMatomoヒートマップのスクリーンショットが壊れます。レンダラーがあなたのホストではなくMatomoのホストを基準にURLを解決するため、画像・スタイルシート・フォントが404になります。原因と対処法を解説します。

Matomoのヒートマップを開いたら、ロゴとヒーロー画像があるべき場所に壊れた画像アイコンが並んでいた。全セクションの背景画像がない。最悪の場合、CSSの存在を忘れたかのようなレイアウトになっている――そんな経験をしたことがあるなら、クリック自体は問題ありません。Matomoは正しい座標に記録しています。壊れているのは、その下にあるスクリーンショットです。ページ上の相対パスを持つものがMatomoのレンダラーへの旅を生き延びられなかったため、実際に訪問者が見ていたページのワイヤーフレームの上に、ヒートマップだけが浮かんでいる状態になります。

症状は一貫しています。https://yoursite.com/images/hero.jpg のような完全修飾URLはきれいに表示されます。相対パス(/images/hero.jpg./logo.svgassets/bg.png)は壊れます。スタイルシート、フォント、スクリプト、インラインスタイル内のCSS url(...) 参照も同様です。このパターンに気づいたら、原因は明らかです。

最も手軽な修正は、私たちが提供している無料のChrome拡張機能 Matomo Heatmap Helper を使うことです。DOMを走査してキャプチャ前にすべての相対URLを絶対URLに変換し、その後元に戻します。この記事では、拡張機能なしで同じことをする方法と、恒久的に対処する修正をいくつか紹介します。

拡張機能なしで修正する方法

キャプチャ直前に問題をカバーするコンソールスニペットと、サイトやビルドに組み込める恒久的な修正がいくつかあります。制約に合うものを選んでください。

ページ上の相対URLをすべて列挙する

何かを変更する前に、実際に何が相対パスになっているかを把握しましょう。ChromeでページをH開き、F12でDevToolsを開いてコンソールに切り替え、以下を貼り付けてください。<img src><img srcset><source srcset><source src><video poster>、インラインスタイルの background-image をすべてカバーします:

js
// 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;
})();

これで、Matomoのレンダラーが誤解決するであろう参照の正確なリストが得られます。テーブルが空なのにヒートマップが壊れている場合は、別の問題です(CDNのCORS、フォントライセンス、スクロールコンテナなど)。そうでなければ、テーブルの各行が絶対URLに書き換えるか移動する必要があるものです。

手軽な修正:キャプチャ直前に相対URLを絶対URLに書き換える

以下は、Matomoのヒートマップキャプチャをトリガーする直前にブラウザのコンソールに貼り付けるスニペットです。拡張機能の relative-url-fixer と同じ動作をします。DOMを走査して各相対リソースURLをページの現在のlocationに対して解決し、絶対バージョンを属性に書き戻します。MatomoがDOMをシリアライズする時点では、すべての参照が完全修飾されており、レンダラーはMatomoからではなくあなたのオリジンからアセットを取得します。<a href> は意図的に除外しています。ナビゲーションリンクを書き換えるとSPAのルーティングが壊れる一方で、スクリーンショットには影響しないからです。

js
// 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.`);
})();

ログの行が表示されたら、すぐにキャプチャしてください。この書き換えは次のナビゲーションまでしか有効ではありません。なお、このスニペットはインラインの style="..." 属性のみを対象としており、外部スタイルシート内の url(...) 呼び出しは見えません。それらについては、後述のポストビルドリライターかビルド時の修正を使ってください。

<head><base> タグを追加する

最もシンプルな恒久的修正です。<head> の先頭に1行追加するだけで、ページ上のすべての相対URLがMatomoのホストではなくあなたのドメインを基準に解決されます:

html
<!-- Edit yoursite.com to your domain -->
<base href="https://yoursite.com/">

注意点は、<base> がリンクターゲットを含むサイト全体のすべての相対URLに影響することです。相対ルーティングに依存しているSPA(React Router、一部の設定のNext.js <Link>、クライアントサイドの pushState を相対パスで使うもの)は、追加後に誤動作する可能性があります。リリース前にナビゲーションフローをテストするか、ヒートマップキャプチャ時のみタグをレンダリングするようにスコープを限定してください(例:トラッキングスクリプトがクエリパラメータで切り替えるなど)。

SPAのナビゲーションを壊さずに <base> タグを追加できるなら、これが答えです。読み進める必要はありません。

ビルドから絶対URLを出力する

<base> が使えない場合は、最初からアセットに絶対URLを出力するようビルドを設定してください。ほとんどのジェネレーターにその設定があります。Next.jsなら next.config.jsassetPrefix、Hugoなら config.tomlbaseURL、Astroなら astro.config.mjssite、Gatsbyなら pathPrefix とビルド時の --prefix-paths を組み合わせます。値をプロダクションドメインに設定してリビルドすれば、ジェネレーターが出力するすべての <img><link><script> タグが完全修飾されます。Matomoのレンダラーはあなたのオリジンから取得し、ヒートマップが正しく動作します。

注意点は、ビルド時の絶対URLは通常、フレームワークが認識しているアセットのみをカバーすることです。手書きのCSS(CMSから来るもの、カスタムテーマに書かれたもの)はしばしば漏れ、コンパイル済みスタイルシート内の url(...) 参照も同様です。ビルドを正しく設定してもヒートマップに背景やアイコンフォントの欠落がある場合、そこが隙間になっています。

CSSの url() 用ポストビルドリライター

ジェネレーターがビルド済みスタイルシート内の url(...) を見落とす場合、以下は出力ディレクトリを走査してルート相対パスをすべて絶対パスに書き換える小さなNode.jsスクリプトです:

js
// 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);
}

CIのポストビルドステップとして組み込んでください。意図的にルート相対パス(/ で始まるパス)のみを対象にしています。./foo../bar の書き換えには、スタイルシートが出力ツリーのどこにあるかを知る必要があるためです。それが多い場合は、上記のビルド時修正の方が適切です。

MatomoがなぜURL相対パスを解決できないか

Matomoのスクリーンショットキャプチャは2段階のプロセスです。まず、トラッカーがライブのDOMをHTMLにシリアライズして送信します。次に、MatomoサーバーがそのHTMLを再レンダリングしてヒートマップビューに表示されるスクリーンショットを生成します。HTMLには書いた通り /images/hero.jpg がそのまま含まれています。しかしレンダラーはあなたのドメインで動いていないため、ブラウザエンジンがその画像を読み込もうとすると、Matomoのホストに対して相対パスを解決します。/images/hero.jpghttps://your-matomo-host/images/hero.jpg になりますが、そこにファイルは存在しません。スタイルシート、フォント、インラインスタイル内の url(...) 呼び出しも同様です。レンダラーはMatomoのサーバーに存在しないファイルを要求し、404が返ってきて、相対パスを持つものがすべて欠落したスクリーンショットになります。

完全修飾URLがうまく動作する理由もこれで明らかです。URLに独自のスキームとホストがあれば、レンダラーはどこから取得するかを正確に知っているため、リクエストは正しくあなたのオリジンに向かいます。

実際に起きていること

よく遭遇するパターンをいくつか:

  • <img src="/images/hero.jpg"> など。最もわかりやすい症状:ヒーロー、ロゴ、商品写真があるべき場所に壊れた画像アイコンが表示される。
  • 相対 href を持つ <link rel="stylesheet" href="/css/main.css">。スタイルシート全体が404になるため、ページがブラウザのデフォルトスタイルでレンダリングされる。「ヒートマップが完全にスタイルなしに見える」と表現されるケースがこれです。
  • インライン style="background-image: url(/images/bg.png)"。背景が消え、レイアウトは一見正常に見えるが視覚的に壊れている。
  • <img srcset><source srcset> 内の相対パスを持つ srcset の候補。Matomoレンダラーがどの候補を選ぶかはビューポートの仮定に依存するため、一方のパスは解決されて他方は解決されないこともあり、壊れ方が断続的に見える。
  • インライン <style> ブロック内や存在しているスタイルシート内で宣言された @font-face src: url("/fonts/inter.woff2")。フォントが404になりシステムデフォルトにフォールバック。(フォントが読み込まれない記事と重複しますが、ここでの原因はCORSではなくパス解決です。)
  • SVG の <use href="/icons/sprite.svg#chevron">。スプライトが読み込まれず、すべてのアイコンが消え、空の <svg> プレースホルダーの周りでレイアウトが崩れる。
  • 相対 href を持つ <link rel="preload" as="font" href="...">。それ自体はスクリーンショットを壊しませんが、同じファミリーの他のアセットも壊れている可能性が高いサインです。

ヒートマップに壊れた画像プレースホルダー、CSSの欠落、ライブサイトと一致しないレイアウトが表示されている場合、これらのうち少なくとも1つが当たっています。同じページで複数が重なることも多いです。

これは同じスクリーンショット内でフォント、画像、スクロールコンテナ、スティッキーヘッダーを壊す問題と同じファミリーでもあります。他の問題についてはMatomoヒートマップのスクリーンショットが壊れる原因に詳しく書いています。フォントが読み込まれない原因画像が読み込まれない原因についても、それぞれ別の記事があります。

私たちが実際にやること

SPAのナビゲーションを壊さずに <base> タグを追加できるなら、それが答えです。1行で、すべての相対URLが正しく解決されて、完了です。

<base> がルーティングと競合する場合は、ビルドをアセットの絶対URL出力に設定して、ビルドが届かない url(...) 参照にはポストビルドCSSリライターを追加してください。少し手間はかかりますが、より徹底的で、フレームワークと戦わずに済みます。

どちらも難しい場合(触れないCMS管理テンプレート、所有していないテーマ、サードパーティが注入するマークアップ)、スタックを変更できないクライアントサイトで私たちが使っているのが Matomo Heatmap Helper Chrome拡張機能です。上記スニペットと同じ書き換えロジックに加え、フォント、画像、スクロールコンテナ、スティッキーヘッダーの同等の修正を、キャプチャのたびに実行します。無料・オープンソースで、コードはGitHubで公開しています

Martez は、この拡張機能が生まれた大きなプロジェクトです。MatomoをMeta AdsとGoogle Adsに接続して、ROAS、CLV、アトリビューションを別のスプレッドシートではなくウェブ解析の隣に置けるようにします。現在プライベートベータ中です。関心があればウェイトリストに登録してください。

ほとんどの相対URL問題はビルド設定一つで解決できます。一度やっておく価値があります。

Related Articles

Matomoヒートマップで相対URLが読み込まれない原因と修正方法 - Martez Blog