First-party
Serve the script from your hostname.
Most blockers match lists, not intent. If the file comes from yoursite.com they never see our hostname. This recovers visits a blunt list threw away. It does not defeat Brave, or someone who turned JavaScript off.
Path proxy — every plan
Pick path or a c. subdomain. Not both. Path is a rewrite on the site you already have:
Rewrite /cf.js and /api/collect on the same host as the website. No DNS. No new certificate. CNAME uncloaking does not apply. The script already POSTs JSON to whatever origin served it — query allowlisted. Collect is POST only.
Open the site in Cookie Free Analytics, pick your host, paste the recipe, hit Check. When we see our marker on your /cf.js, the snippet switches itself.
Path rewrites recover blockers. They also send collect through the host, so uniques, geo, and the 60/min flood cap see the proxy, not each visitor. CNAME is the install that keeps visitor IP, city, and same-day uniques.
Pick how the site is built
- WordPress — official plugin prints the snippet. First-party is a c. CNAME. There is no Shopify app; paste the theme snippet.
- Webflow, Framer, Squarespace, Wix, Ghost — paste the snippet in Custom Code. First-party is a c. CNAME; those hosts will not rewrite a path.
- Vercel, Netlify, Cloudflare, nginx — one rewrite. Recipes below. Prefer CNAME if you care about uniques and locations.
Subdomain — CNAME
Skip this if you already use a path rewrite. One method per site — not both.
c.yoursite.com CNAME to cname.vercel-dns.com — DNS only, grey cloud. We attach that hostname to our European servers and issue HTTPS. Prefer a short name, not analytics or tracking. Orange-cloud without our certificate will fail the check.
GoDaddy
- Domain Portfolio → the domain → DNS.
- Add New Record → CNAME.
- Name is the prefix only (
c), not the full host. - Value:
cname.vercel-dns.com. TTL 1 hour. Save.
That only works if GoDaddy still hosts DNS. If nameservers are Cloudflare, add the record there instead.
Rewrite recipes
Vercel
{
"rewrites": [
{ "source": "/cf.js", "destination": "https://www.cookiefreeanalytics.com/cf.js" },
{ "source": "/cf-clicks.js", "destination": "https://www.cookiefreeanalytics.com/cf-clicks.js" },
{ "source": "/api/collect", "destination": "https://www.cookiefreeanalytics.com/api/collect" }
]
}rewrites, not redirects. HTTP 200 on your hostname. Put these three first, before any catch-all to index.html. A 301/308 to www.cookiefreeanalytics.com is what blockers see. Vercel cannot strip Cookie on a rewrite — we do not store it. CNAME keeps visitor IP, geo, and uniques.
Netlify
/cf.js https://www.cookiefreeanalytics.com/cf.js 200 /cf-clicks.js https://www.cookiefreeanalytics.com/cf-clicks.js 200 /api/collect https://www.cookiefreeanalytics.com/api/collect 200
Status 200 is the point — a 301 still exposes our hostname to blockers. Path proxies share one egress IP, so uniques and geo follow the proxy. CNAME keeps the visitor.
Cloudflare
export default {
async fetch(request, _env, _ctx) {
const url = new URL(request.url);
if (url.pathname === "/cf.js" || url.pathname === "/cf-clicks.js" || url.pathname === "/api/collect") {
url.hostname = "www.cookiefreeanalytics.com";
const headers = new Headers(request.headers);
headers.delete("cookie");
headers.delete("authorization");
return fetch(url, { method: request.method, headers, body: request.body });
}
return fetch(request);
}
};Attach the worker to your apex (or www). A transform rule that 301s is not enough.
nginx / Caddy
# nginx
location = /cf.js { proxy_pass https://www.cookiefreeanalytics.com/cf.js; proxy_ssl_server_name on; proxy_set_header Cookie ""; }
location = /cf-clicks.js { proxy_pass https://www.cookiefreeanalytics.com/cf-clicks.js; proxy_ssl_server_name on; proxy_set_header Cookie ""; }
location = /api/collect { proxy_pass https://www.cookiefreeanalytics.com; proxy_ssl_server_name on; proxy_set_header Cookie ""; proxy_set_header Authorization ""; }
# Caddy
handle /cf.js { reverse_proxy https://www.cookiefreeanalytics.com }
handle /api/collect { reverse_proxy https://www.cookiefreeanalytics.com }Reload the server, then check.
Paste recipes
WordPress
Plugins → Add New → Upload Plugin https://github.com/PrestonsCreekCapital/Cookie-Free-Analytics-WP/archive/refs/tags/v1.0.2.zip Activate. Settings → Cookie Free Analytics. Paste the public ID. Or paste before </head> or </body>: <script defer src="https://www.cookiefreeanalytics.com/cf.js" data-site="cf_YOUR_ID"></script> First-party is a c. CNAME (Subdomain), not a path rewrite through PHP.
The plugin prints the snippet. CNAME keeps uniques and geo. WordPress.com cheap plans: paste instead.
Shopify
Online Store → Themes → ⋯ → Edit code Open Layout / theme.liquid Paste just above </body>: <script defer src="https://www.cookiefreeanalytics.com/cf.js" data-site="cf_YOUR_ID"></script> Save.
That records visits. First-party on Shopify is a c. CNAME. We do not ship a Shopify app.
Webflow
Site settings → Custom Code → Footer Code <script defer src="https://www.cookiefreeanalytics.com/cf.js" data-site="cf_YOUR_ID"></script> Save → Publish.
Webflow cannot rewrite /cf.js on normal plans. First-party is a c. CNAME.
Framer
Site settings → General → Custom Code → End of <body> <script defer src="https://www.cookiefreeanalytics.com/cf.js" data-site="cf_YOUR_ID"></script> Publish.
Framer cannot reverse-proxy /cf.js. First-party is a c. CNAME.
Squarespace
Settings → Developer tools → Code Injection → Footer <script defer src="https://www.cookiefreeanalytics.com/cf.js" data-site="cf_YOUR_ID"></script> Save.
No path rewrite on Squarespace. First-party is a c. CNAME.
Wix
Settings → Custom Code → Add Code Place: Body — end Load on: All pages <script defer src="https://www.cookiefreeanalytics.com/cf.js" data-site="cf_YOUR_ID"></script> Apply.
Wix cannot host /cf.js for you. First-party is a c. CNAME.
Ghost
Settings → Code injection → Site footer <script defer src="https://www.cookiefreeanalytics.com/cf.js" data-site="cf_YOUR_ID"></script> Save.
Ghost(Pro) has no reverse proxy. Self-hosted Ghost can use the nginx recipe. Otherwise: a c. CNAME.
Questions
- How does the beacon leave the browser?
- POST JSON via navigator.sendBeacon. The payload is not in the URL, so CDN access logs do not see ?email= or ?token=. The shipped script does not GET. Collect is POST only — GET returns 204 and writes nothing.
- Which query parameters do you keep?
- utm_source, utm_medium, utm_campaign, utm_content, utm_term. The script allowlists them. The server allowlists them again. Everything else is dropped before it leaves the tab.
- Do you store the full referrer?
- No. Origin + path, no query. Source labels (Google, Hacker News, Direct) come from the hostname. Tokens in a referrer query never land on the row.
- Does it count client-side / SPA navigations?
- Yes. pushState, replaceState, popstate. The same path is not counted twice. Campaign tags stick to the first pageview in the tab — inner routes do not inherit them. After the landing, the referrer we send is the previous path on your host, so those hits are Direct, not another HN session.
- How small is the file with all of that?
- Under 1 KB gzipped, measured from /cf.js on this build. HMAC is server-side. POST, the allowlist, SPA hooks, and a prerender skip are in the script. Auto click-listeners are not.
- Do prefetch and bots inflate the count?
- Prerender/prefetch is dropped in the script (document.prerendering) and again if Sec-Purpose says prefetch. Library and crawler user-agents are dropped. Hostname must belong to the site, except on the public demo. Trailing slashes are folded so /pricing and /pricing/ are one path.
- Do you store city-level location?
- The edge already classified the request. We copy country, ISO 3166-2 region (as US-CA so California is not Canada), and a city name. We do not store IP, latitude, longitude, or postal code, and we do not run MaxMind or any other geo-IP database. If the host sent nothing, the fields are empty.
- What stops someone flooding /api/collect?
- Public ingest is not unforgeable. 60 beacons per hashed IP per minute — one flood, not a busy site. The counter lives in Postgres, so every Frankfurt isolate shares it. Over that IP quota we return 204 and write nothing. A popular site is not dropped. Raw IP is not stored on that row.
The wizard lives on each site. What the script sends · How a visitor is counted.