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.
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.
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, andmatomo.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.
Renamed file: still blocked
- 1
Browser
- analytics.yourcompany.com2
Ad blocker inspects the request
hostname ruleHit dropped
First-party proxy: gets through
- 1
Browser
- example.com/app-sync2
Nginx on your domain
- proxy_pass3
Matomo matomo.php
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.
# 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:
<!-- 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]:
[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'sproxy_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:
[General]
proxy_client_headers[] = HTTP_CF_CONNECTING_IP
proxy_client_headers[] = HTTP_X_FORWARDED_FORMatomo 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 is wasted if the IP reaching it is your proxy's.
Verify it actually worked
Three checks, in order:
- 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 toexample.comrather than the Matomo host, and both return200/204. - 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.
- 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
srcand 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 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 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 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 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 if that's relevant.
Renaming the file was never going to work. Moving the request is.