Go to all posts

Making a privacy respecting hit counter with Plausible analytics

Quick notice before you continue

As of 11-1-2023 the following code seems to only work when run on local. Will update this post when I have a solution figured out.

As of 11-13-2023, it is working. I have updated the code below to match the code that is currently working.

Remember Neocities? Remember Geocities?? I missed out on that craze, but I love looking back on that style of website.

Clashing fonts, way too many gifs, "Under Construction" banners that never went away. Beautiful stuff.

So here's how I'm going to bring that back, using Plausible and their privacy respecting API.

Jump to heading What is Plausible?

Plausible is an analytics platform that respects user privacy. No IP tracking, no persistent cookie, nothing skeezy. It only shows me how many people visit your site, what site they came from, what pages they visit, which the leave from. Simple as can be.

They also offer an API.

Jump to heading What do we want?

Page views. That's it.

Going to their API Documentation there's a couple of endpoints like timeseries, breakdown, visitors. What we need is aggregate.

My site is built with 11ty, so I create a new file in my _data directory called stats.js. I'm using the .js extension so that I can dynamically pull in the pageview numbers.

For caching it, we use the tried and true @11ty/eleventy-fetch. This looks something like this:

const EleventyFetch = require('@11ty/eleventy-fetch');

const siteId = 'ginger.wtf';
const endpoint = 'https://plausible.io/api/v1/stats/aggregate';

module.exports = async function() {
const requestUrl = `${endpoint}?site_id=${siteId}&period=6mo&metrics=pageviews`;

return EleventyFetch(requestUrl, {
type: 'json',
duration: '1d',
});
}

We require our dependency and add required parameters, those being site_id, period, and metrics.

The documentation lists the different time formats, but none of them are "all time". I need more data.

To fix this, we update the period parameter to period=custom. This means we also need to add a date parameter.

I started using Plausible on November 1st, 2022. The date format used in the parameter is the same as what new Date().toISOString() returns.

The url must be properly encoded too, so we put our date range through an encodeURIComponent.

Here's our updated snippet:

const EleventyFetch = require('@11ty/eleventy-fetch');

const siteId = 'ginger.wtf';
const endpoint = 'https://plausible.io/api/v1/stats/aggregate';

const plausibleStart = '2022-11-01';

// toISOString returns something like this: 2023-11-01T21:21:26.654Z
// so we split on the `T` for the date only
const plausibleEnd = new Date().toISOString().split('T')[0]
const range = encodeURIComponent(`${plausibleStart},${plausibleEnd}`);

module.exports = async function() {
const requestUrl = `${endpoint}?site_id=${siteId}&period=custom&date=${range}&metrics=pageviews`;


return EleventyFetch(requestUrl, {
type: 'json',
duration: '1d'
});
}

Jump to heading Authorization

Plausible's API uses the Bearer Token authorization method. Open up your user settings and generate an API token. Now drop that in a .env file. Also remember to add .env to your .gitignore if it isn't already there!

We want to grab that with JavaScript so that we don't expose any other data. Install dotenv as a dependency and include it in our script. Also add in the proper headers option to the EleventyFetch.

const EleventyFetch = require('@11ty/eleventy-fetch');

require('dotenv').config();
const token = process.env.AUTHORIZATION;

const siteId = 'ginger.wtf';
const endpoint = 'https://plausible.io/api/v1/stats/aggregate';

const plausibleStart = '2022-11-01';

// toISOString returns something like this: 2023-11-01T21:21:26.654Z
// so we split on the `T` for the date only
const plausibleEnd = new Date().toISOString().split('T')[0]
const range = encodeURIComponent(`${plausibleStart},${plausibleEnd}`);

module.exports = async function() {
const requestUrl = `${endpoint}?site_id=${siteId}&period=custom&date=${range}&metrics=pageviews`;

return EleventyFetch(requestUrl, {
type: 'json',
duration: '1d',
fetchOptions: {
headers: {
Authorization: 'Bearer '+token,
}
}
});
}

Jump to heading Final touches on our data

The fetch works! If it didn't work for you, double check their guide with PostMan

The result is wrapped in an object though. This means our data looks like this:

{
"results": {
"pageviews": {
"value": 200
}
}
}

Not a fan.

Here's my fix:

const EleventyFetch = require('@11ty/eleventy-fetch');
require('dotenv').config();


const siteId = 'ginger.wtf';
const token = process.env.AUTHORIZATION;
const endpoint = 'https://plausible.io/api/v1/stats/aggregate';

const plausibleStart = '2022-11-01';

// toISOString returns something like this: 2023-11-01T21:21:26.654Z
// so we split on the `T` for the date only
const plausibleEnd = new Date().toISOString().split('T')[0]
const range = encodeURIComponent(`${plausibleStart},${plausibleEnd}`);

module.exports = async function() {
const requestUrl = `${endpoint}?site_id=${siteId}&period=custom&date=${range}&metrics=pageviews`;

const fetchObj = await EleventyFetch(requestUrl, {
type: 'json',
duration: '1d',
fetchOptions: {
headers: {
Authorization: 'Bearer '+token,
}
}
});

return fetchObj.results
}

Jump to heading Using a fallback

Even though everyone reading this post is a perfect developer and has never coded a bug in their life, lets create a fallback anyways.

const EleventyFetch = require('@11ty/eleventy-fetch');
require('dotenv').config();


const siteId = 'ginger.wtf';
const token = process.env.AUTHORIZATION;
const endpoint = 'https://plausible.io/api/v1/stats/aggregate';

const plausibleStart = '2022-11-01';

// toISOString returns something like this: 2023-11-01T21:21:26.654Z
// so we split on the `T` for the date only
const plausibleEnd = new Date().toISOString().split('T')[0]
const range = encodeURIComponent(`${plausibleStart},${plausibleEnd}`);

module.exports = async function() {
const requestUrl = `${endpoint}?site_id=${siteId}&period=custom&date=${range}&metrics=pageviews`;

const eleventyFetchOptions = {
type: 'json',
duration: '1d',
fetchOptions: {
headers: {
Authorization: 'Bearer '+token,
}
}
}

let fetchObj = {
results: {
pageviews: {
value: ':('
}
}
}

try {
fetchObj = await EleventyFetch(requestUrl, eleventyFetchOptions);
} catch(e) {
console.error('Error getting Plausible Stats: ', e.message);
} finally {
return fetchObj.results;
}

return fetchObj.results
}

Jump to heading Making our hit counter

This is what the code looks like:

<span class="hit-counter">
hits: {{ stats.pageviews.value }}
</span>

Yeah, really, it's that simple. The data cascade in 11ty is powerful.

If you are doing this yourself, you should really explore what all is available through the Plausible API. It's a great service, worth every penny. There's a sense of peace knowing you're not selling your users data or breaking the law by using your analytics.