Skip to main content
bortox.it - Andrea Bortolotti
  1. Welcome on BBlog/

Show page visitor count on Hugo static site with Congo theme and Goatcounter analytics

·712 words·4 mins·
tutorial hugo goatcounter congo visitor count analytics

In this tutorial I’ll use the Goatcounter json api to get the current page views for each article in my hugo static site, and show views in the article meta, in this case next to the reading time, or just below, as you can see. It’s like those old sites with a visit counter on the bottom.

Number of page views:

The Goatcounter API #

GoatCounter is the only service I use to gather analytics about my website, as of 2022.

Branded visitor counter #

To enable the visitor counter, open GoatCounter settings and check Allow adding visitor counts on your website.

More info on GoatCounter’s Documentation

On the free version you can add a - branded - visitor counter, in SVG/HTML/PNG format, but (in my opinion) it is ugly and needs extra css code to work with dark and light theme, instead of a simple text string, obtainable via the json api.

<script>
    // Append to the <body>; can use a CSS selector to append somewhere else.
    window.goatcounter.visit_count({append: 'body'})
</script>
<script data-goatcounter="https://MYCODE.goatcounter.com/count"
        async src="//gc.zgo.at/count.js"></script>

Visitor counter using JSON API #

Another way is to use the JSON API and request how many pageviews -- has this webpage by sending a GET request to https://DOMAIN.goatcounter.com/counter/ + page path + .json.

<div>Number of visitors: <div id="stats"></div></div>
<script>
    var r = new XMLHttpRequest();
    r.addEventListener('load', function() {
        document.querySelector('#stats').innerText = JSON.parse(this.responseText).count_unique
    })
    r.open('GET', 'https://MYCODE.goatcounter.com/counter/' + encodeURIComponent(location.pathname) + '.json')
    r.send()
</script>

Process on this website #

This works, but only in this page. If I have a list of pages in Hugo and want to get the visits for each of them, location.pathname will tell me the path of the current page, not of the pages listed on the current page. Some Hugo magic is needed to make it work also on cateogry and tag pages.

  1. Edit /layouts/partials/article-meta.html to add partial meta/visit-counter.html
  2. Write partial meta/visit-counter.html
  3. Edit i18n files to add new titles (optional)

Add visit-counter meta to article-meta #

Article meta
This is the article meta, containing page date, word count, reading time and visitor count.

I copied the original /layouts/partials/article-meta.html file from the hugo Congo theme that I am using and added the following lines.

{{ if and (.Params.showVisitCount | default (.Site.Params.article.showVisitCount | default true)) }}
{{ $meta.Add "partials" (slice (partial "meta/visit-counter.html" .)) }}
{{ end }}

These lines basically read:

  • If the showVisitCount parameter is true, show the visit-counter.html partial and add it to $meta.

  • If showVisitCount parameter is not listed, default the condition to true and add visit-counter.html partial to $meta.

I added the lines before the line with showEdit since I wanted the visit count before the button to edit the page on Github, like this (2115 visite means 2115 visits):

Write article-meta.html #

<span id="{{ .File.UniqueID }}" title="{{ i18n "article.visit_title" }}">
    {{- "πŸ™ˆ" | markdownify | emojify -}}
</span>
<script>
    var r = new XMLHttpRequest();
    r.addEventListener('load', function() {
        document.getElementById('{{ .File.UniqueID }}').innerText = JSON.parse(this.responseText).count_unique + ' ' + {{ i18n "article.visit_name" }}
    })
    r.open('GET', 'https://bortox.goatcounter.com/counter/' + encodeURIComponent({{ .RelPermalink }}.replace(/(\/)?$/, '')) + '.json')
    r.send()
</script>
{{- /* Trim EOF */ -}}

Since this meta also apperead on category and tag pages, I couldn’t use “stats” as the id of the text to be edited, the id had to be unique for each page.

So, I made use of Hugo’s functionalities and set as id the {{ .File.UniqueID }} of every page, basically the MD5-checksum of the content file’s path.

I used .RelPermalink (the relative permanent link for the page) of every page to build the request to Goatcounter API.

Update: I added .replace(/(\/)?$/, '') to .RelPermalink because Goatcounter was marking pages ending with a slash as not found and registering views for pages without the last slash.

There is probably an easier way to do this with cleaner Javascript, if you have some ideas or have to point out that something is not correct, contact me at bortox at bortox dot it. I will add soon commenting functionality, but not with Disqus.

So we have to translate the span title, which is basically the tooltip of the text, and the visit name. For example, if I have English language to set up, I’ll edit i18n/en.yaml and add the following lines under the article section:

article:
    visit_title: "Page views"
    visit_name: "visits"