First-Party Analytics: Serve PageDuel From Your Own Domain

By default the PageDuel snippet loads from cdn.pageduel.com and sends data to pageduel.com. That's the fastest way to install — but two things can get in the way: a strict Content-Security-Policy that only allows 'self', and ad-blockers that filter third-party analytics domains. A first-party install sidesteps both: your site serves the script and receives the data calls on its own domain, then quietly forwards them to PageDuel through a small reverse proxy.

How it works

You add three forwarding rules to your site, then use the first-party install tag (the installer on your analytics dashboard generates it prefilled for your domain — pick First-party above the snippet):

<script async src="https://yourdomain.com/pd.js" data-key="YOUR_SITE_KEY" data-api="https://yourdomain.com"></script>

Every path stays on your domain and forwards as follows:

  • /pd.jshttps://cdn.pageduel.com/pd.js — the analytics script
  • /pd-experiments.jshttps://cdn.pageduel.com/pd-experiments.js — loaded automatically next to pd.js when you run experiments
  • /api/s/*https://pageduel.com/api/s/* — config, events, and install verification

No snippet changes are needed beyond the tag itself: data-api points the data calls at your domain, and the experiments bundle always loads from wherever pd.js was served.

Next.js

Add the rewrites to next.config.js (or next.config.ts). Rewrites proxy the request server-side — visitors only ever talk to your domain.

// next.config.js
module.exports = {
  async rewrites() {
    return [
      { source: "/pd.js", destination: "https://cdn.pageduel.com/pd.js" },
      { source: "/pd-experiments.js", destination: "https://cdn.pageduel.com/pd-experiments.js" },
      { source: "/api/s/:path*", destination: "https://pageduel.com/api/s/:path*" },
    ];
  },
};

Vercel (any framework)

For non-Next.js projects on Vercel, put the same rules in vercel.json at the project root.

{
  "rewrites": [
    { "source": "/pd.js", "destination": "https://cdn.pageduel.com/pd.js" },
    { "source": "/pd-experiments.js", "destination": "https://cdn.pageduel.com/pd-experiments.js" },
    { "source": "/api/s/:path*", "destination": "https://pageduel.com/api/s/:path*" }
  ]
}

Netlify

Status 200 makes these proxies (not redirects). netlify.toml shown; the same rules work in a _redirects file.

[[redirects]]
  from = "/pd.js"
  to = "https://cdn.pageduel.com/pd.js"
  status = 200

[[redirects]]
  from = "/pd-experiments.js"
  to = "https://cdn.pageduel.com/pd-experiments.js"
  status = 200

[[redirects]]
  from = "/api/s/*"
  to = "https://pageduel.com/api/s/:splat"
  status = 200

Cloudflare Worker

Attach the Worker to the routes yourdomain.com/pd.js, yourdomain.com/pd-experiments.js, and yourdomain.com/api/s/* — everything else passes through untouched.

export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === "/pd.js" || url.pathname === "/pd-experiments.js") {
      return fetch(new Request("https://cdn.pageduel.com" + url.pathname, request));
    }
    if (url.pathname.startsWith("/api/s/")) {
      return fetch(new Request("https://pageduel.com" + url.pathname + url.search, request));
    }
    return fetch(request);
  },
};

nginx

Inside your server block. proxy_ssl_server_name is required so the upstream TLS handshake sends the right hostname.

location = /pd.js {
  proxy_pass https://cdn.pageduel.com/pd.js;
  proxy_set_header Host cdn.pageduel.com;
  proxy_ssl_server_name on;
}

location = /pd-experiments.js {
  proxy_pass https://cdn.pageduel.com/pd-experiments.js;
  proxy_set_header Host cdn.pageduel.com;
  proxy_ssl_server_name on;
}

location /api/s/ {
  proxy_pass https://pageduel.com/api/s/;
  proxy_set_header Host pageduel.com;
  proxy_ssl_server_name on;
}

Apache

Requires mod_proxy and mod_proxy_http (a2enmod proxy proxy_http). Place inside your VirtualHost.

SSLProxyEngine on
ProxyPreserveHost Off

ProxyPass "/pd.js" "https://cdn.pageduel.com/pd.js"
ProxyPass "/pd-experiments.js" "https://cdn.pageduel.com/pd-experiments.js"
ProxyPass "/api/s/" "https://pageduel.com/api/s/"

Good to know

  • POST bodies must pass through. Events arrive as POST /api/s/e with a JSON body — every recipe above forwards method and body untouched. If you write your own proxy, make sure it does too.
  • Your CSP only needs 'self'. With everything first-party, script-src 'self' and connect-src 'self' are enough — no PageDuel domains in the allowlist.
  • Headers don't need special handling. PageDuel validates proxied requests without relying on the Origin header, so you don't have to rewrite or preserve it.
  • Verify in a minute. Deploy the rules, paste the first-party tag, then watch the installer on your analytics dashboard — it flips to “live” on the first pageview, and its Re-check button re-inspects your CSP after changes.

Analytics that survive the real web

PageDuel gives you privacy-friendly analytics, A/B testing, and revenue attribution from one snippet — now installable fully first-party.