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.js→https://cdn.pageduel.com/pd.js— the analytics script/pd-experiments.js→https://cdn.pageduel.com/pd-experiments.js— loaded automatically next topd.jswhen 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 = 200Cloudflare 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/ewith 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'andconnect-src 'self'are enough — no PageDuel domains in the allowlist. - Headers don't need special handling. PageDuel validates proxied requests without relying on the
Originheader, 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.