# Why renaming matomo.php doesn't stop ad blockers (serve Matomo Tag Manager first-party instead)

> Renaming matomo.php to a neutral path doesn't help: ad blockers match on hostname and path together, so as long as hits go to analytics.yourcompany.com the rule still fires. The durable fix is to serve Matomo Tag Manager fully first-party through a reverse proxy. Here's how, including the Cloudflare IP gotcha that collapses every visitor to one location.

Published: 2026-08-12
Categories: Matomo Tracking
Canonical: https://martez.io/blog/matomo-tag-manager-first-party-adblockers

---

You renamed `matomo.php` to something neutral, pushed it live, and the ad blocker still eats your hits. uBlock Origin, AdGuard, Brave's built-in shield: toggle one on and the visit count drops the same way it did before you touched anything. The rename felt like the obvious move, and it did nothing.

It did nothing because blockers don't match on the filename alone. They match on two things at once, and renaming the path only deals with one of them. As long as the browser is firing requests at `analytics.yourcompany.com`, a hostname rule catches them no matter what you called the endpoint. The fix that actually holds is to send every tracking request to the same domain as the site itself, over a neutral path, so the browser sees nothing but ordinary first-party traffic to `example.com`. A reverse proxy on your own web server forwards those requests to Matomo without the browser ever knowing.

<Callout type="tip" title="TL;DR">

Renaming `matomo.php` doesn't help because ad blockers match on the hostname too, so hits going to `analytics.yourcompany.com` still get caught regardless of the path. The fix that holds is to serve both Matomo Tag Manager assets first-party through a reverse proxy on your own domain: the container loader and the tracking endpoint, on neutral paths with no tracking words in them. Ship the visitor-IP forwarding (`proxy_client_headers[]` and `proxy_ips[]`) in the same change, or every visitor will geolocate to wherever your server lives.

</Callout>

## Why the rename didn't work

Modern blocker lists test each request against two kinds of pattern:

- Hostname patterns like `analytics.*`, `stats.*`, `matomo.*`, `piwik.*`, and the well-known Matomo Cloud hosts.
- Path and filename patterns like `matomo.php`, `piwik.php`, `/matomo/`, `container_*.js`, and `matomo.js`.

Renaming `matomo.php` to `collect.php` dodges the second list, and not even all of it, since `collect` is a flagged word in its own right. The request is still going to a hostname that announces itself as analytics, so the hostname rule fires and the hit never leaves the browser. You can rename the file forever; the host gives you away every time.

First-party serving removes both signals at once. When the loader and the tracking endpoint both live on `example.com`, there's no separate analytics hostname to match, and you pick path names with no tracking words in them. At the network level the tracking hit then looks like any other request to your own site. That isn't a permanent guarantee: a filter list can always add your specific path later, or match on a request signature. What it buys you is taking away the obvious hostname and default-path signals that catch you today, which is what recovers most of the lost hits. Two browser-facing assets have to move first-party for this to work: the container script, which by default loads from `https://matomo-host/js/container_XXXXXXXX.js`, and the tracking endpoint, which by default is `matomo.php`. Move both onto your domain and the requests look like any other first-party call.

<FlowLanes
  caption="Renaming the endpoint still resolves to an analytics hostname the blocker matches. Routing through your own domain removes that signal."
  lanes={[
    {
      title: "Renamed file: still blocked",
      tone: "fail",
      steps: [
        { title: "Browser" },
        {
          title: "Ad blocker inspects the request",
          arrow: "analytics.yourcompany.com",
          branch: { label: "hostname rule", title: "Hit dropped" },
        },
      ],
    },
    {
      title: "First-party proxy: gets through",
      tone: "ok",
      steps: [
        { title: "Browser" },
        { title: "Nginx on your domain", arrow: "example.com/app-sync" },
        { title: "Matomo matomo.php", arrow: "proxy_pass", tone: "ok" },
      ],
    },
  ]}
/>

None of this touches Matomo core, and none of it overrides consent. Your CMP and Do-Not-Track handling work exactly as before. That last point is the one that always comes up, so there's a section on it further down.

## Step 1: proxy two neutral paths on each tracked site

On every domain you track, add a reverse proxy that forwards two paths to your Matomo server. Here's the Nginx version; Apache's `mod_proxy` and Caddy do the same thing with their own syntax.

```nginx
# First-party tracking endpoint -> Matomo's matomo.php
location = /app-sync {
    proxy_pass https://MATOMO_HOST/matomo.php;
    proxy_set_header Host MATOMO_HOST;
    proxy_ssl_server_name on;
    proxy_ssl_name MATOMO_HOST;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $host;
}

# First-party container script -> Matomo's container JS
location = /app-loader.js {
    proxy_pass https://MATOMO_HOST/js/container_XXXXXXXX.js;
    proxy_set_header Host MATOMO_HOST;
    proxy_ssl_server_name on;
    proxy_ssl_name MATOMO_HOST;
}
```

The path names are the whole game. Pick endpoints with no tracking vocabulary in them: avoid `analytics`, `matomo`, `stats`, `track`, `pixel`, `beacon`, and `collect`. Names like `/app-sync`, `/app-loader.js`, `/api-event`, or `/core.js` blend into normal site traffic and give a filter list nothing generic to match. Swap your real Matomo host in for `MATOMO_HOST` (the bare hostname like `analytics.yourcompany.com`, not a full `https://` URL) and your real container filename in for `container_XXXXXXXX.js`, which you'll find in the embed snippet Tag Manager hands you. The two `proxy_ssl_*` lines matter when Matomo is reached over HTTPS on shared or virtual-hosted TLS, which covers Matomo Cloud and most managed hosts: without them Nginx leaves SNI off the upstream handshake and the connection either fails or lands on the wrong certificate.

## Step 2: point the container at the first-party endpoint

In Matomo, open Tag Manager → your container → Variables and edit the Matomo Configuration variable. There are two fields to set. Point Matomo URL at your first-party base (`https://example.com/`), and set Tracking Request Target Path to the neutral endpoint from Step 1 (`app-sync`). That second field defaults to `matomo.php`, and it exists for exactly this reason; Matomo's own docs describe it as a way to aim hits at a proxy or a custom endpoint. Publish the container when you're done. Hits now go to `https://example.com/app-sync?...`, and Nginx forwards them internally to `matomo.php`. The browser never sees the Matomo host.

## Step 3: serve the container script first-party

The loader in your site's `<head>` still points at the Matomo host until you change it. Swap the `src`:

```html
<!-- before -->
<script src="https://MATOMO_HOST/js/container_XXXXXXXX.js"></script>

<!-- after -->
<script src="https://example.com/app-loader.js"></script>
```

Now both assets the browser touches, the script that bootstraps tracking and every hit it sends, resolve to your own domain. Keep the cache on `/app-loader.js` short, around an hour, so that when you publish a container change in Tag Manager browsers pick it up quickly instead of serving a stale copy for a day.

## Step 4: give Matomo the real visitor IP back

Here's the gotcha that catches everyone the second first-party serving starts working. Once the proxy sits in front of Matomo, every hit arrives from the proxy's IP address. Matomo geolocates that one IP, and your entire audience suddenly appears to live in a single city, wherever your server happens to be. The data is flowing again. It's just all pointing at your data centre.

Fix it on the Matomo server in `config/config.ini.php`, under `[General]`:

```ini
[General]
proxy_client_headers[] = HTTP_X_FORWARDED_FOR
proxy_host_headers[]    = HTTP_X_FORWARDED_HOST
proxy_ips[]             = 203.0.113.10   ; your proxy's public IP, not the visitor's
```

`proxy_client_headers[]` tells Matomo which header carries the visitor IP, the one your Nginx block set back in Step 1, instead of reading the address it's connecting from. `proxy_ips[]` is the part people skip, but it isn't a trust switch. It's the list of your own proxy and load-balancer IPs that Matomo should step past when it walks the forwarded chain to find the real visitor; without it, the proxy's own address can end up chosen as the client. List your proxy's address there, and only addresses you control.

Spoofing is a separate worry, and it's handled back in Step 1. Because the Nginx block sets `X-Forwarded-For` to `$remote_addr`, the address Nginx itself saw the request come from, it overwrites anything the client sent. Nobody can hand you a forged `X-Forwarded-For` and fake their location into your reports. If instead you append to the incoming header, which is what the common `$proxy_add_x_forwarded_for` does, a client-supplied value rides along inside the chain, and depending on your Matomo version it can be the one that gets picked. Overwrite at the edge unless you genuinely have a trusted proxy or CDN in front whose forwarded values you need to keep.

If you front the proxy with Cloudflare, the real client IP isn't in `X-Forwarded-For` at all; it's in Cloudflare's own header. Add that one instead, or ahead of the standard header:

```ini
[General]
proxy_client_headers[] = HTTP_CF_CONNECTING_IP
proxy_client_headers[] = HTTP_X_FORWARDED_FOR
```

Matomo checks these in order and stops at the first one with a value, so listing the Cloudflare header first means CF-fronted hits resolve correctly while anything arriving without it falls back to the standard forwarded header. That order is only safe if your origin can't be reached directly: lock the server down to Cloudflare's IP ranges (or strip any client-supplied `CF-Connecting-IP` at your edge), or a visitor hitting the box straight on could set that header themselves. If your geolocation runs off the GeoIP2 database, this header config is the input it reads; a [correct GeoIP2 / GeoLite2 setup](/blog/matomo-geoip2-geolite2-setup) is wasted if the IP reaching it is your proxy's.

## Verify it actually worked

Three checks, in order:

1. Open a tracked page with your ad blocker on. In DevTools, Network tab, confirm the loader (`/app-loader.js`) and the hit (`/app-sync`) both resolve to `example.com` rather than the Matomo host, and both return `200`/`204`.
2. In Matomo, go to Visitors, then Visits Log, and confirm new visits show the right country instead of your data centre's. That's how you know Step 4 took.
3. Toggle the blocker off and on a few times. The visit count should hold steady. If it still drops, something is still resolving to the Matomo host; recheck the loader `src` and the Tracking Request Target Path.

If you proxy everything correctly but get a gateway error on the endpoint instead of a `204`, that's a proxy-layer problem, not a Matomo one. We wrote up the [502s that show up self-hosting Matomo behind a reverse proxy](/blog/self-host-matomo-reverse-proxy-502) separately.

## The official PHP fallback

If you'd rather not maintain Nginx rules, or you don't control the web-server config on every site you track, Matomo ships an official [tracker proxy](/blog/matomo-http-tracking-api-server-side) that gets you to the same place. You download the proxy's PHP files (`matomo.php`, `piwik.php`, `proxy.php`, and `matomo-proxy.php`, plus the Heatmaps config file if you run that plugin) from Matomo's repository, drop them on your domain, and authenticate them server-side with a `token_auth`. Same first-party result, reached through PHP instead of the web-server layer. It's heavier to replicate across a lot of sites and it adds a PHP hop to every hit, which is why we reach for the reverse proxy first, but it's the documented route when editing Nginx isn't an option.

## Does this bypass consent?

No, and it's worth saying plainly, because it's the first thing a privacy-minded teammate will ask. First-party serving changes where the request goes, not whether it's allowed to fire. Consent and Do-Not-Track are enforced inside Matomo and your CMP no matter which domain the hit lands on. If a visitor hasn't consented, moving the endpoint onto your own domain doesn't quietly make tracking happen anyway. This is a measure against blocking, not a way around consent. If [cookieless or consent-gated tracking](/blog/matomo-consent-cookieless-tracking) is part of your setup, it keeps working exactly as you configured it.

## What we'd actually do

Serve both assets first-party through the reverse proxy. It's the highest-impact thing you can do against ad blockers: it removes the hostname signal and the path signal together, it needs no changes to Matomo core, and one Nginx block per site copies cleanly across however many domains feed your central install. Save the official PHP tracker proxy for the sites where you can't touch the web-server config.

And don't ship Step 1 without Step 4. The most common version of this story is someone getting tracking through the proxy, celebrating the recovered hits, and a week later wondering why every visitor lives in Frankfurt. Put the forwarded headers and the `proxy_ips[]` line in the same change, check the country in the Visits Log before you call it done, and you're finished.

[Martez](/?utm_source=martez&utm_medium=blog&utm_campaign=matomo-tag-manager-first-party-adblockers) connects Matomo with Meta Ads and Google Ads so ROAS, CLV, and attribution sit next to your web analytics instead of in a separate spreadsheet. It's in private beta. [Join the waitlist](/signup?utm_source=martez&utm_medium=blog&utm_campaign=matomo-tag-manager-first-party-adblockers) if that's relevant.

Renaming the file was never going to work. Moving the request is.
