Me

Lodybo

Proxying scripts in Remix: how to stop worrying about ad blockers and to love Plausible

January 3, 2023January 3, 2023About 5 minutes

I recently switched to Plausible for my analytics as a full-featured and more privacy-friendly alternative to Google Analytics. Plausible has a lot to offer (I recommend reading their website about all the details) and offers a simple one-script solution for hooking up to their analytics service:

<script defer data-domain="yourdomain.com" src="https://plausible.io/js/script.js"></script>

However, this approach has its drawbacks. Plausible is blocked by some blocklist maintainers who believe every tracking code should be blocked, and not pass judgment over whether that tracking code is either good or bad.

Plausible goes over a lot more details about this, but the main giveaway is that Plausible is way more privacy-friendly than GA, but "suffers" the consquences of blocklist maintainers who really want to block every tracking script.

You probably know all of this, or else you'd probably not read an article detailing how to proxy the Plausible script in Remix just so that you can have analytics. If not, that's more than okay and I applaud your thirst for curiosity and simultaneously hope to have pointed you towards a very good Google Analytics alternative. I use Plausible on all my projects, including this blog.

Come on, let's move on!

Yeah, you're right! A way to "circumvent" the block is to serve the Plausible script as a first-party asset on your domain. There are a number of ways to do this, as Plausible demonstrates on their website, but Remix is still missing from that list.

A vert simple solution is to simply download the file at https://plausible.io/js/script.js, put it in the public/ folder and reference it in root.tsx. This, however, has 2 drawbacks:

  1. You'd probably miss out on any future updates of script.js.
  2. You'd still need to find a way to proxy calls to https://plausible.io/api/event

I spent some time thinking about a solution for these problems until it finally hit me: using Remix's resource routes. I can proxy both the script and the requests to /event from a loader function in a route.

A small reintroduction to rsource routes: in Remix a resource route is a route without a rendered component and can export an action or loader function for POST/PUT and GET requests. Aside from the examples the Remix team gives us, we can also use resource routes for additional purposes:

  • Circumventing CORS errors on back-end services for your client who don't have Access-Control headers set.
  • Implementing proxies to other servers.

We're going to focus on the second use case: implementing a proxy to Plausible's servers for the analytics script file, and the /event/ API. This way we circumvent the ad blocker by serving these as "first party" asset on our domain, while in reality  we're fetching them from Plausible's servers.

Let's start with creating the resource route for https://plausible.io/js/script.js.

Proxying the script

I created a file at app/routes/js/script[.js].ts. The [.js] part is added to the file name in order to make sure that Remix will serve the file at domain.com/js/script.js. You can fill it with the following contents:

export const loader = async () => {
  const plausibleScriptData = await fetch('https://plausible.io/js/script.js');
  const script = await plausibleScriptData.text();
  const { status, headers } = plausibleScriptData;

  return new Response(script, {
    status,
    headers,
  });
};

That's it (for the script part). We simply create a loader function that fetches the script.js file from the Plausible servers and returns it to the browser. We extract the contents of the file (using await plausibleScriptData.text();) and pass it as the new body. We also pass the headers and status code from the request to Plausible's servers. Doing this means that we're only the middleman: we're not making any assumptions about the status codes or headers, we're simply using the ones defined by Plausible.

It also means we get the proper caching headers for free. We get the one defined from Plausible (at the time of writing: public, must-revalidate, max-age=86400) and we serve it to the browser. The browser will then execute the caching directives, which means it won't revalidate for 24 hours and then recheck with the server. Our server, which proxies the call to Plausible's server. Plausible's server then answers with "the file has been updated" (which prompts a refetch) or "the file is still valid" which tells the browser to reset the counter on the 24 hour revalidation period.

Proxying events

Of course, the fun starts with https://plausible.io/api/event. We'll start by creating a route at app/routes/api/event.ts. Plausible has this URL coded in their script.js so that's the reason we use it in our app. The contents are a bit more complex.

import type { ActionArgs } from '@remix-run/node';

export const action = async ({ request }: ActionArgs) => {
  const { method, body } = request;

  const response = await fetch('https://plausible.io/api/event', {
    body,
    method,
    headers: {
      'Content-Type': 'application/json',
    },
  });
  const responseBody = await response.text();
  const { status, headers } = response;

  return new Response(responseBody, {
    status,
    headers,
  });
};

The /api/event call should also simply pass-through everything Plausible wants, because we shouldn't make a lot of assumptions about how Plausible works. So we create an action function (because calls to /api/event are POST requests), where we extract the method and body. Body is obvious because that's the payload Plausible will receive as analytics data, and method is to make sure we don't miss any sneaky PUT requests. Not that I've come across them but still, I'd like to make as little assumptions as possible.

We proceed by making a fetch to Plausible's servers passing along the method, body and our Content-Type: 'application/json' header.

We deconstruct the reponse body into a string and proceed to check the status code. We then simply create a new response, passing the proxied response status code, headers and body to our application.

If Plausible would return a 400 or 500 error code, we'd return it to the browser which would alert us in the console. Otherwise, everything would be fine and dandy and Plausible would simply work.

And, also, we get the benefit of having the correct caching headers, should Plausible ever change them. Which they probably won't but you never know..

Referencing the script file

The last piece of the puzzle is in root.tsx, or  in the file where you have defined the HTML skeleton structure. In my case I created an app/components/Document.tsx component which I use inside root.tsx and in a number of error and catch boundaries.

Add the following line between the <head/> tags:

<script defer data-domain="yourdomain.com" src="/js/script.js"></script>

Remix will fetch the script file at your resource route, which will fetch the script file that's currently on the Plausible server. That script file will be returned to the browser, which loads and parses it (as deferred, so it will not block the rest of the document load). The script file will do a request to /api/event.ts, which is also proxied by Remix and will be passed-through to Plausible's API. The response will be returned to the browser which will finally end up and say:

200 ok

And there you have it: we have proxied a script file and an API call from our Remix back-end to a different server in order to circumvent ad blockers. This is just a single example of using a resource route to a script file and/or an API.

Happy days!