Proxy Setup
Ad blockers sometimes block third-party analytics scripts. By proxying BetterMeter through your own domain, the script and events appear as first-party requests -- making them invisible to blockers. This works because the browser sees requests to yourdomain.com/bm/... instead of bettermeter.com/api/....
How it works
You set up URL rewrites on your server so that three paths on your domain forward to BetterMeter:
/bm/s→ https://bettermeter.com/api/sTracker script/bm/e→ https://bettermeter.com/api/eEvent endpoint/bm/p→ https://bettermeter.com/api/pNoscript pixelThen update your tracking snippet to use the proxied paths:
<script defer data-site="example.com" data-api="/bm/e" src="/bm/s"></script>
<img src="/bm/p?s=example.com" alt="" style="position:absolute;width:0;height:0;overflow:hidden" />The data-api attribute tells the tracker to send events to your proxy endpoint instead of directly to BetterMeter. The src loads the script from your proxy. To the browser (and ad blockers), everything is a same-origin request.
Platform guides
Next.js
module.exports = {
async rewrites() {
return [
{ source: "/bm/s", destination: "https://bettermeter.com/api/s" },
{ source: "/bm/e", destination: "https://bettermeter.com/api/e" },
{ source: "/bm/p", destination: "https://bettermeter.com/api/p" },
];
},
};Nuxt
export default defineNuxtConfig({
routeRules: {
"/bm/s": { proxy: "https://bettermeter.com/api/s" },
"/bm/e": { proxy: "https://bettermeter.com/api/e" },
"/bm/p": { proxy: "https://bettermeter.com/api/p" },
},
});SvelteKit
const config = {
kit: {
// SvelteKit doesn't have built-in rewrites.
// Use a server hook instead:
},
};import type { Handle } from "@sveltejs/kit";
const PROXY_MAP: Record<string, string> = {
"/bm/s": "https://bettermeter.com/api/s",
"/bm/e": "https://bettermeter.com/api/e",
"/bm/p": "https://bettermeter.com/api/p",
};
export const handle: Handle = async ({ event, resolve }) => {
const target = PROXY_MAP[event.url.pathname];
if (target) {
const url = new URL(target);
url.search = event.url.search;
const res = await fetch(url, {
method: event.request.method,
headers: event.request.headers,
body: event.request.method !== "GET" ? event.request.body : undefined,
// @ts-expect-error — needed for streaming
duplex: "half",
});
return new Response(res.body, {
status: res.status,
headers: res.headers,
});
}
return resolve(event);
};Astro
import type { APIRoute } from "astro";
const PROXY_MAP: Record<string, string> = {
s: "https://bettermeter.com/api/s",
e: "https://bettermeter.com/api/e",
p: "https://bettermeter.com/api/p",
};
export const ALL: APIRoute = async ({ params, request }) => {
const target = PROXY_MAP[params.proxy ?? ""];
if (!target) return new Response("Not found", { status: 404 });
const url = new URL(target);
url.search = new URL(request.url).search;
return fetch(url, {
method: request.method,
headers: request.headers,
body: request.method !== "GET" ? request.body : undefined,
// @ts-expect-error — needed for streaming
duplex: "half",
});
};Remix / React Router
import type { LoaderFunctionArgs, ActionFunctionArgs } from "@remix-run/node";
const PROXY_MAP: Record<string, string> = {
s: "https://bettermeter.com/api/s",
e: "https://bettermeter.com/api/e",
p: "https://bettermeter.com/api/p",
};
async function proxy({ request, params }: LoaderFunctionArgs | ActionFunctionArgs) {
const target = PROXY_MAP[params["*"] ?? ""];
if (!target) return new Response("Not found", { status: 404 });
const url = new URL(target);
url.search = new URL(request.url).search;
return fetch(url, {
method: request.method,
headers: request.headers,
body: request.method !== "GET" ? request.body : undefined,
// @ts-expect-error — needed for streaming
duplex: "half",
});
}
export const loader = proxy;
export const action = proxy;Nginx
location /bm/s {
proxy_pass https://bettermeter.com/api/s;
proxy_ssl_server_name on;
proxy_set_header Host bettermeter.com;
}
location /bm/e {
proxy_pass https://bettermeter.com/api/e;
proxy_ssl_server_name on;
proxy_set_header Host bettermeter.com;
}
location /bm/p {
proxy_pass https://bettermeter.com/api/p;
proxy_ssl_server_name on;
proxy_set_header Host bettermeter.com;
}Apache
RewriteEngine On
RewriteRule ^bm/s$ https://bettermeter.com/api/s [P,L]
RewriteRule ^bm/e$ https://bettermeter.com/api/e [P,L]
RewriteRule ^bm/p$ https://bettermeter.com/api/p [P,L]
# Requires mod_proxy and mod_proxy_http enabled:
# a2enmod proxy proxy_httpCaddy
example.com {
handle_path /bm/s {
reverse_proxy https://bettermeter.com/api/s {
header_up Host bettermeter.com
}
}
handle_path /bm/e {
reverse_proxy https://bettermeter.com/api/e {
header_up Host bettermeter.com
}
}
handle_path /bm/p {
reverse_proxy https://bettermeter.com/api/p {
header_up Host bettermeter.com
}
}
}Cloudflare Workers
export default {
async fetch(request) {
const url = new URL(request.url);
const map = {
"/bm/s": "https://bettermeter.com/api/s",
"/bm/e": "https://bettermeter.com/api/e",
"/bm/p": "https://bettermeter.com/api/p",
};
const target = map[url.pathname];
if (!target) return fetch(request);
const dest = new URL(target);
dest.search = url.search;
return fetch(dest.toString(), {
method: request.method,
headers: { ...Object.fromEntries(request.headers), Host: "bettermeter.com" },
body: request.method !== "GET" ? request.body : undefined,
});
},
};Vercel (without Next.js)
{
"rewrites": [
{ "source": "/bm/s", "destination": "https://bettermeter.com/api/s" },
{ "source": "/bm/e", "destination": "https://bettermeter.com/api/e" },
{ "source": "/bm/p", "destination": "https://bettermeter.com/api/p" }
]
}Netlify
[[redirects]]
from = "/bm/s"
to = "https://bettermeter.com/api/s"
status = 200
force = true
[[redirects]]
from = "/bm/e"
to = "https://bettermeter.com/api/e"
status = 200
force = true
[[redirects]]
from = "/bm/p"
to = "https://bettermeter.com/api/p"
status = 200
force = trueWordPress
Add these rewrite rules to your theme's functions.php or a custom plugin:
add_action('init', function() {
add_rewrite_rule('^bm/s$', 'index.php?bm_proxy=s', 'top');
add_rewrite_rule('^bm/e$', 'index.php?bm_proxy=e', 'top');
add_rewrite_rule('^bm/p$', 'index.php?bm_proxy=p', 'top');
});
add_filter('query_vars', function($vars) {
$vars[] = 'bm_proxy';
return $vars;
});
add_action('template_redirect', function() {
$proxy = get_query_var('bm_proxy');
if (!$proxy) return;
$map = [
's' => 'https://bettermeter.com/api/s',
'e' => 'https://bettermeter.com/api/e',
'p' => 'https://bettermeter.com/api/p',
];
if (!isset($map[$proxy])) return;
$url = $map[$proxy];
if ($_SERVER['QUERY_STRING']) $url .= '?' . $_SERVER['QUERY_STRING'];
$args = ['method' => $_SERVER['REQUEST_METHOD'], 'timeout' => 10];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$args['body'] = file_get_contents('php://input');
$args['headers'] = ['Content-Type' => $_SERVER['CONTENT_TYPE'] ?? 'application/json'];
}
$response = wp_remote_request($url, $args);
if (is_wp_error($response)) { status_header(502); exit; }
status_header(wp_remote_retrieve_response_code($response));
foreach (wp_remote_retrieve_headers($response) as $k => $v) {
if (in_array(strtolower($k), ['content-type', 'cache-control'])) header("$k: $v");
}
echo wp_remote_retrieve_body($response);
exit;
});
// After adding, flush rewrite rules: Settings → Permalinks → SaveDjango
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path("bm/<slug:endpoint>", views.bm_proxy),
]
# views.py
import requests
from django.http import HttpResponse
PROXY_MAP = {
"s": "https://bettermeter.com/api/s",
"e": "https://bettermeter.com/api/e",
"p": "https://bettermeter.com/api/p",
}
def bm_proxy(request, endpoint):
target = PROXY_MAP.get(endpoint)
if not target:
return HttpResponse("Not found", status=404)
url = target + ("?" + request.META["QUERY_STRING"] if request.META.get("QUERY_STRING") else "")
resp = requests.request(
method=request.method, url=url,
headers={"Host": "bettermeter.com", "Content-Type": request.content_type},
data=request.body if request.method == "POST" else None,
timeout=10,
)
return HttpResponse(resp.content, status=resp.status_code, content_type=resp.headers.get("Content-Type"))Rails
# config/routes.rb
match "/bm/:endpoint", to: "bm_proxy#proxy", via: [:get, :post], constraints: { endpoint: /[sep]/ }
# app/controllers/bm_proxy_controller.rb
class BmProxyController < ApplicationController
skip_before_action :verify_authenticity_token
PROXY_MAP = { "s" => "/api/s", "e" => "/api/e", "p" => "/api/p" }.freeze
def proxy
path = PROXY_MAP[params[:endpoint]]
return head(:not_found) unless path
uri = URI("https://bettermeter.com#{path}")
uri.query = request.query_string.presence
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = (request.post? ? Net::HTTP::Post : Net::HTTP::Get).new(uri)
req["Host"] = "bettermeter.com"
if request.post?
req.body = request.body.read
req["Content-Type"] = request.content_type
end
resp = http.request(req)
render body: resp.body, status: resp.code.to_i, content_type: resp["Content-Type"]
end
endVerification
After setting up the proxy, verify it works:
# Should return the tracker JavaScript
curl -s https://yourdomain.com/bm/s | head -5
# Should return 202 (event accepted)
curl -s -o /dev/null -w "%{http_code}" -X POST https://yourdomain.com/bm/e \
-H "Content-Type: application/json" \
-d '{"site_id":"yourdomain.com","event_name":"test"}'