Proxying Plausible Analytics using Cloudflare Pages

I’ve been using Plausible on this site to track basic visitor metrics. Plausible is a light-weight1, privacy-friendly2 alternative to Google Analytics. It allows for anonymous site measurement without the use of cookies or collection of personal data. It avoids persistent identifiers, preventing cross-site or cross-device tracking.

While popular browsers like Safari and Firefox don’t block Plausible, a growing number of users deploy ad-blockers to safeguard against intrusive ads and trackers. If you haven’t already adopted an ad-blocker, I strongly advocate for uBlock Origin. Despite Plausible’s commitment to user privacy, it occasionally gets flagged by blocklist maintainers. Such decisions arise from the inherent difficulty in differentiating “friendly” trackers from the “unfriendly” ones.


Integrating Plausible directly into your site means potential blocks by ad-blockers. For example, even though I have a self-hosted instance running on, it’s blocked because it has analytics in the URL.

I really appreciate ad-blockers aggressively blocking trackers and analytics. However, in this case, it hinders me from the harmless gathering of metrics. Proxying offers a workaround. Instead of Plausible receiving requests directly, they first go through our domain before they’re forwarded to Plausible. By addressing requests to a trusted domain (the user’s current website), this method reduces the likelihood of being blocked by ad-blockers.

The rest of this post can be considered a guide on how to set up proxying when hosting your website on Cloudflare Pages. It assumes you have a website set up and are signed in to a Plausible account, whether that’s on a self-hosted instance or

Cloudflare Pages

When it comes to generating static websites, Hugo is my go-to tool. It’s what powers this blog. While I’ve relied on a server equipped with Nginx for this site—employing a dash of Nginx configuration to establish analytics proxying3—there are even simpler methods that utilise free hosting platforms. I’ve had some experience with Vercel and more recently Netlify when building Muscat Tech.

Cloudflare Pages is a strong alternative with a generous free tier; all requests to static websites are free. In comparison to Netlify’s super simple approach to proxying, using a _redirects file4, Cloudflare’s setup is a little more involved. Although Cloudflare Pages supports a redirects file, it has a limitation: it doesn’t allow proxying to external URLs5.

Plausible has guides in their documentation for how to set up proxying for different platforms. For Cloudflare, they have a guide that requires creating Cloudflare Workers6. When you already have a static site on Cloudflare Pages, integrating Cloudflare Functions into your project becomes more straightforward than following their Workers guide.

Cloudflare Functions

Functions are essentially the same thing as Workers. They provide a serverless execution environment to add dynamic functionality to an existing static website.

Where a Worker is a single script that captures all requests, Functions provide file-based routing. The directory structure inside /functions dictates the routes on which these Functions execute. For instance, with a functions/hello.js file, visiting would trigger the code (minus the js suffix). For Plausible proxying, we need /js/script.js and /api/event routed from our domain.

  • Project Root
    • functions
      • js
        • script.js.ts
      • api
        • event.ts
    • Other files...


Cloudflare’s runtime7 permits both Javascript and Typescript. My preference leans towards Typescript, given its advantages.

Consider the functions/js/script.js.ts contents below. Although it has a .ts extension, it maps to The logic checks for a cached response; failing that, it fetches and caches before sending a response.

interface Env {

export const onRequestGet: PagesFunction<Env> = async ({
}) => {
	let response = await caches.default.match(request)
	if (!response) {
		response = await fetch(`${env.PLAUSIBLE_BASE_URL}/js/script.js`)
		waitUntil(caches.default.put(request, response.clone()))
	return response

The /api/event endpoint code, stored in /functions/api/event.ts, simply proxies the request.

interface Env {

export const onRequestPost: PagesFunction<Env> = async (context) => {
	const request = new Request(context.request)
	return await fetch(`${context.env.PLAUSIBLE_BASE_URL}/api/event`, request)

Environment Variables

Instead of embedding the Plausible URL directly, I’ve opted for an environment variable. You configure these on the Cloudflare dashboard where you can set distinct values for production and preview deployments.

Pages settings on Cloudflare dashboard

Setting environment variables in Pages settings

Further Configuration

On a purely static project, Cloudflare Pages offers unlimited free requests. However, once we add Functions to a project, all requests by default will invoke the Function. Functions are free to use up to 100K requests monthly. To continue receiving unlimited free static requests, Cloudflare suggests adding a _routes.json file to the output directory.

In my case the json file that was autogenerated by Cloudflare was identical to the one I wanted to write. The file tells Cloudflare to only invoke the Function for /js/script.js and /api/event requests.

  "version": 1,
  "description": "Generated by wrangler@3.7.0",
  "include": [
  "exclude": []

Wrapping Up

To wrap things up, modify the site’s HTML script tags URLs so they’re relative to our domain:

<script defer data-domain="" data-api="/api/event" src="/js/script.js"></script>

With everything in place, head back to your Plausible dashboard. You’ll start observing unblocked events streaming in.


By utilizing Cloudflare Pages and Functions to proxy Plausible Analytics, you can ensure accurate, unblocked metrics collection while upholding user privacy. This method eliminates the need for maintaining a server solely for proxying requests, and with Cloudflare’s generous free tier, it remains cost-effective for a vast majority of websites.