Infrastructure

Ajouter un compteur de visiteurs GoatCounter à un site Jekyll sans requête réseau côté navigateur

Contourner les blocages CORS/bloqueurs sur les endpoints GoatCounter en récupérant le nombre de visiteurs côté serveur, au moment du build Jekyll, plutôt que depuis le navigateur du visiteur.

Table des matières

GoatCounter est une alternative légère et respectueuse de la vie privée à Google Analytics. Afficher un simple compteur “visiteurs / mois” sur la page d’accueil semble trivial — en pratique, chaque approche côté navigateur se heurte à un blocage différent. Ce guide documente le problème et la solution qui fonctionne réellement : tout récupérer côté serveur, au moment du build.

Ce qui ne fonctionne pas

L’API stats officielle (/api/v0/stats/total) exige systématiquement un token Bearer, même avec le dashboard configuré en public :

curl -s https://MONCODE.goatcounter.com/api/v0/stats/total
# → {"error":"no Authorization header"}

Le endpoint public /counter/*.json, pensé pour être embarqué sans token, fonctionne en curl mais casse en fetch() depuis le navigateur :

Access to fetch at 'https://moncode.goatcounter.com/counter//.json?...'
from origin 'https://mondomaine.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present

La variante <img> avec /counter/*.svg contourne en théorie le CORS (une image n’y est pas soumise) — mais échoue quand même avec un 499/net::ERR_FAILED, reproductible aussi bien sur PC que sur mobile, sans lien avec un bloqueur de pub local.

La solution : fetch côté serveur, au build

Plutôt que de faire confiance au navigateur du visiteur, un plugin Jekyll récupère le chiffre une seule fois, pendant jekyll build/jekyll serve, et l’injecte directement dans le HTML statique généré — plus aucune requête réseau au chargement de la page.

# _plugins/goatcounter_stats.rb
require "net/http"
require "json"
require "date"

Jekyll::Hooks.register :site, :after_reset do |site|
  code = site.config.dig("analytics", "goatcounter_code")
  next if code.nil? || code.empty?

  begin
    end_date   = Date.today
    start_date = end_date - 30
    uri = URI("https://#{code}.goatcounter.com/counter//.json?start=#{start_date}&end=#{end_date}")

    Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: 5, read_timeout: 5) do |http|
      res = http.get(uri.request_uri)
      if res.is_a?(Net::HTTPSuccess)
        data = JSON.parse(res.body)
        site.config["goatcounter_visitors"] = (data["count_unique"] || data["count"] || "0").to_i
      end
    end
  rescue StandardError => e
    Jekyll.logger.warn "goatcounter_stats:", "fetch failed (#{e.message})"
  end
end
ca-certificates sur Alpine

Une image ruby:3.3-alpine ne contient pas les certificats CA par défaut — Net::HTTP en HTTPS échoue silencieusement (ou plante le process Jekyll au démarrage) tant que le paquet ca-certificates n’est pas installé dans le Dockerfile.

Affichage côté template

Le widget se contente de lire une valeur déjà calculée — aucun script, aucun appel réseau côté visiteur :

<div class="stat-card">
  {% if site.goatcounter_visitors %}
    {% if site.goatcounter_visitors >= 1000 %}
      <div class="stat-value">{{ site.goatcounter_visitors | divided_by: 1000.0 | round: 1 }}k</div>
    {% else %}
      <div class="stat-value">{{ site.goatcounter_visitors }}</div>
    {% endif %}
  {% else %}
    <div class="stat-value">—</div>
  {% endif %}
  <div class="stat-label">Visitors / month</div>
</div>

Configuration

# _config.yml
analytics:
  goatcounter_code: "moncode"

Vérification

docker compose -f docker-compose.public.yml up -d --build
docker logs infops-public | grep -i goatcounter   # aucune erreur = OK

curl -s http://localhost:4000/ | grep -A3 "Visitors / month"
# → le chiffre doit apparaître directement dans le HTML, pas de "—"

Résultat

  • Aucun appel réseau côté navigateur : ni CORS, ni bloqueur, ni 499 possible.
  • Le chiffre est déjà présent dans le HTML au moment où la page arrive chez le visiteur.
  • Contrepartie assumée : la valeur ne se met à jour qu’au prochain build/rechargement, pas en temps réel — largement suffisant pour un compteur mensuel.

Comments