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, GET only as a stripped fallback.
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.
Pick how the site is built
- WordPress — upload a plugin. It prints the snippet and serves
/cf.jsfrom your domain. - Shopify — paste a theme snippet. First-party is a CNAME or an app proxy.
- 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.
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.
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.
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
1. Download the Cookie Free Analytics plugin (button below). 2. In WordPress: Plugins → Add New → Upload Plugin → Activate. 3. Settings → Cookie Free Analytics if you need to change the site ID. 4. Back here, press Check now. WordPress.com (hosted, no plugins) cannot do this. Use the c. CNAME instead.
The plugin prints the snippet and proxies /cf.js from your domain. That is the whole setup.
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, or a custom app proxy — see /docs/shopify.
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. The collect URL still accepts a stripped GET if something else calls it.
- 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.