I used Matomo for many years as an analytics platform for my websites. two problems here: I am not fond of MySQL/MariaDB (I’m most of a PostgreSQL fan) and Matomo is overkill for my usage. Time to move to GoatCounter!

Let’s put things in place: we have a server that hosts the Ghost website and the GoatCounter service. The website at https://blog.example.com/ and the GoatCounter dashboard at https://goatcounter.example.com/. The normal way to use GoatCounter from the website is to put the following lines in the site footer injection field of the Ghost admin panel:

<script 
    data-goatcounter="https://goatcounter.example.com/count" async
    src="//goatcounter.example.com/count.js">
</script>
<noscript>
    <img src="https://goatcounter.example.com/count?p=/test-img">
</noscript>

All is hosted on the same server, so we can do better, first to be more elegant, and second to be more stealth, because even if GoatCounter is in my opinion an ethical web analytics platform, it is in the scope of adblockers, and I am a big user of uBlock Origin myself, so I understand it perfectly. But well, I have to try :)

The trick is to proxy_pass from the Ghost vhost to the GC vhost, like this (in the Ghost vhost block):

# Stealth GoatCounter https://github.com/zgoat/goatcounter
location = /count {
    proxy_set_header Host goatcounter.example.com;
    proxy_pass http://goatcounter;
    proxy_set_header X-Real-IP $remote_addr;
    expires epoch;
}
location = /count.js {
    root /usr/local/www/goatcounter/public;
    expires max;
}

We can see that /count is proxied, and /count.js is served directly from the GoatCounter installation (but we could proxy it too).and proxy_pass http://goatcounter refers to an upstream block which is globally available, for example in /etc/nginx/conf.d/upstream_goatcounter.conf:

upstream goatcounter {
        server 127.0.0.1:6666;
        keepalive 64;
}

Our injected code is now:

<script
    data-goatcounter="https://blog.example.com/count" async 
    src="//blog.example.com/count.js">
</script>
<noscript>
    <img src="https://blog.example.com/count?p=/test-img">
</noscript>

Everything is served from the same domain.

Smart Tracking Pixel

But wait! We can see a problem with the tracking pixel. Every hit from this tracking pixel generates a /test-img entry in the GoatCounter dashboard. We have to replace this string with the current page URL. We could do it by putting the <noscript> content directly in our theme template, and using {{url}} as the p= value, but it is not very practical.

Instead, we are making a nice name for this pixel, with a legit .gif extension, and we are taking advantage of the power of Nginx to extract the parent page URL from the image call. Let’s put a new block in the Ghost vhost:

location ~ /hello.gif$ {
    rewrite ^(.+)hello.gif$ /count?p=$1 break;
    proxy_set_header Host goatcounter.example.com;
    proxy_pass http://goatcounter;
    proxy_set_header X-Real-IP $remote_addr;
    expires epoch;
}

And a slightly modified <noscript> line:

<noscript>
    <img src="hello.gif">
</noscript>

Now the tracking pixel hello.gif is called relatively from the parent page, so if we have a post at https://blog.example.com/test-post/, the pixel will be loaded at https://blog.example.com/test-post/hello.gif.Nginx will rewrite internally this as /count?p=/test-post/ and proxy it to GoatCounter.

Of course, don’t use a real image with the name hello.gif in your website :)


I didn’t test this hack very extensively, and I don’t know how it behaves with RSS or mailed pages. If you find issues, English errors or others mistakes, please comment!