11

Configuration du proxy

Les bloqueurs de publicités bloquent parfois les scripts d'analytique tiers. En relayant BetterMeter via votre propre domaine, le script et les événements apparaissent comme des requêtes de première partie -- les rendant invisibles aux bloqueurs. Cela fonctionne car le navigateur voit des requêtes vers yourdomain.com/bm/... au lieu de bettermeter.com/api/....

Comment ça fonctionne

Vous configurez des réécritures d'URL sur votre serveur pour que trois chemins de votre domaine redirigent vers BetterMeter :

/bm/s→ https://bettermeter.com/api/sScript de suivi
/bm/e→ https://bettermeter.com/api/eEndpoint d'événements
/bm/p→ https://bettermeter.com/api/pPixel noscript

Ensuite, mettez à jour votre code de suivi pour utiliser les chemins du proxy :

Proxied snippet
<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" />

L'attribut data-api indique au script d'envoyer les événements à votre endpoint proxy au lieu de directement à BetterMeter. Le src charge le script depuis votre proxy. Pour le navigateur (et les bloqueurs de publicités), tout est une requête de même origine.

Guides par plateforme

Next.js

next.config.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

nuxt.config.ts
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

svelte.config.js
const config = {
  kit: {
    // SvelteKit doesn't have built-in rewrites.
    // Use a server hook instead:
  },
};
src/hooks.server.ts
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

src/pages/bm/[...proxy].ts
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

app/routes/bm.$.tsx
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

nginx.conf
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

.htaccess or httpd.conf
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_http

Caddy

Caddyfile
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

worker.js
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)

vercel.json
{
  "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

netlify.toml
[[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 = true

WordPress

Ajoutez ces règles de réécriture au functions.php de votre thème ou à un plugin personnalisé :

functions.php
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 → Save

Django

urls.py + views.py
# 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 + controller
# 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
end

Vérification

Après avoir configuré le proxy, vérifiez qu'il fonctionne :

Test
# 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"}'