Ce que font vraiment vos visiteurs (et comment le savoir sans payer Hotjar)
Développement Web

Ce que font vraiment vos visiteurs (et comment le savoir sans payer Hotjar)

11 mai 20266 min de lecturePar Ahmad Al-Kardali
Retour au blog

Ce que font vraiment vos visiteurs (et comment le savoir sans payer Hotjar)

Un client m'a appelé il y a quelques semaines. Son site tourne bien, il a du trafic, mais personne ne le contacte. Google Analytics lui dit que des gens viennent. Pas grand chose de plus.

C'est le problème avec les analytics par défaut : ils comptent les entrées, pas ce qui se passe à l'intérieur. Vous savez que quelqu'un est venu sur /tarifs. Vous ne savez pas s'il a lu jusqu'en bas, combien de temps il est resté, s'il a cliqué sur votre bouton de contact ou s'il a commencé à remplir votre formulaire avant de partir.

La solution classique c'est d'installer Hotjar ou Mixpanel. Sauf que ça coûte cher, ça rajoute des cookies à gérer côté RGPD, et honnêtement la plupart des gens n'ouvrent jamais les rapports parce que c'est trop complexe.

Sur ce site j'ai mis en place quelque chose de beaucoup plus simple. Quatre événements custom avec @vercel/analytics, deux composants dans le layout, et une modification dans le formulaire de contact. Voilà ce que ça donne.


Pourquoi @vercel/analytics plutôt qu'autre chose

Si votre site est sur Next.js et déployé sur Vercel, la librairie est déjà dans votre projet. Elle est gratuite, elle n'utilise pas de cookies tiers, et l'API tient en une ligne :

import { track } from "@vercel/analytics";
track("nom_event", { données: "utiles" });

Tout remonte dans Vercel Dashboard sous "Custom Events". Pas de nouveau compte à créer, pas de script externe à charger.


Tracker les clics sans tout réécrire

Mon premier réflexe aurait été d'ajouter un onClick sur chaque lien important du site. C'est l'approche la plus lente et la plus fragile, vous en oublierez toujours un.

J'ai préféré un seul composant qui écoute tous les clics au niveau du document. Ça marche pour tous les liens existants et futurs, sans y toucher.

// components/analytics/LinkTracker.tsx
"use client";

import { track } from "@vercel/analytics";
import { useEffect } from "react";

export default function LinkTracker() {
  useEffect(() => {
    const handleClick = (e: MouseEvent) => {
      const target = (e.target as Element).closest("a");
      if (!target) return;

      const href = target.getAttribute("href");
      if (!href || href.startsWith("#")) return;

      const isExternal = href.startsWith("http") || href.startsWith("mailto");
      const label =
        target.getAttribute("aria-label") ||
        target.textContent?.trim().slice(0, 50) ||
        href;
      const source = target.getAttribute("data-track-source");

      track("link_click", {
        href,
        label,
        type: isExternal ? "external" : "internal",
        ...(source ? { source } : {}),
      });
    };

    document.addEventListener("click", handleClick);
    return () => document.removeEventListener("click", handleClick);
  }, []);

  return null;
}

Le closest("a") c'est pour les cas où on clique sur une icône SVG à l'intérieur d'un lien. Sans ça, e.target pointe sur l'icône et non le lien parent.

Pour les CTAs importants où vous voulez savoir depuis quelle section de la page les gens cliquent, il suffit d'ajouter un attribut data-track-source :

<a href="#contact" data-track-source="navbar">Contactez-moi</a>
<a href="#contact" data-track-source="hero">Démarrer un projet</a>

Scroll depth et temps passé sur une page

Ces deux données ensemble donnent une image assez précise de l'engagement. Quelqu'un qui reste 3 minutes sur /tarifs et descend à 90% de la page, c'est un lead sérieux. Quelqu'un qui repart après 8 secondes à 20% de scroll, il n'a pas accroché.

J'ai mis les deux dans le même composant. Il se réinitialise automatiquement à chaque changement de route grâce à usePathname.

// components/analytics/PageTracker.tsx
"use client";

import { track } from "@vercel/analytics";
import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";

export default function PageTracker() {
  const pathname = usePathname();
  const startTime = useRef(Date.now());
  const milestones = useRef(new Set<number>());

  useEffect(() => {
    startTime.current = Date.now();
    milestones.current = new Set();
  }, [pathname]);

  useEffect(() => {
    const handleScroll = () => {
      const scrolled = window.scrollY + window.innerHeight;
      const total = document.documentElement.scrollHeight;
      const pct = Math.round((scrolled / total) * 100);

      for (const milestone of [25, 50, 75, 100]) {
        if (pct >= milestone && !milestones.current.has(milestone)) {
          milestones.current.add(milestone);
          track("scroll_depth", { page: pathname, depth: `${milestone}%` });
        }
      }
    };

    window.addEventListener("scroll", handleScroll, { passive: true });
    return () => window.removeEventListener("scroll", handleScroll);
  }, [pathname]);

  useEffect(() => {
    const handleVisibility = () => {
      if (document.visibilityState === "hidden") {
        const seconds = Math.round((Date.now() - startTime.current) / 1000);
        if (seconds > 2) track("time_on_page", { page: pathname, seconds });
      } else {
        startTime.current = Date.now();
      }
    };

    document.addEventListener("visibilitychange", handleVisibility);
    return () => document.removeEventListener("visibilitychange", handleVisibility);
  }, [pathname]);

  return null;
}

Le passive: true sur le scroll listener indique au navigateur qu'on ne va pas bloquer le défilement, il peut optimiser le rendu sans attendre notre callback.


L'abandon de formulaire, la donnée qu'on ignore toujours

C'est probablement la plus utile et la moins installée. Quelqu'un qui commence à remplir votre formulaire de contact, c'est quelqu'un d'intéressé. S'il repart sans envoyer, vous avez perdu un lead potentiel et vous n'en savez rien.

L'événement visibilitychange se déclenche quand l'utilisateur change d'onglet, minimise le navigateur ou ferme la page. C'est le signal le plus fiable pour détecter un départ.

const hasInteracted = useRef(false);
const wasSubmitted = useRef(false);

useEffect(() => {
  const handleVisibility = () => {
    if (
      document.visibilityState === "hidden" &&
      hasInteracted.current &&
      !wasSubmitted.current
    ) {
      track("form_abandoned", { form: "contact" });
    }
  };

  document.addEventListener("visibilitychange", handleVisibility);
  return () => document.removeEventListener("visibilitychange", handleVisibility);
}, []);

On marque hasInteracted à true dès que l'utilisateur clique dans le formulaire via onFocus, et wasSubmitted à true après un envoi réussi. Si le visiteur part et que wasSubmitted est encore à false, l'abandon est enregistré.


Dans le layout

import { Analytics } from "@vercel/analytics/react";
import LinkTracker from "@/components/analytics/LinkTracker";
import PageTracker from "@/components/analytics/PageTracker";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <LinkTracker />
        <PageTracker />
      </body>
    </html>
  );
}

Ce que vous en faites

Les données ne servent à rien sans décision derrière. Si vous voyez que la majorité des visiteurs quittent /tarifs avant 50% de scroll, vos informations importantes sont trop bas sur la page. Si le taux d'abandon du formulaire est élevé, le formulaire est probablement trop long ou demande trop d'informations d'entrée.

C'est ce genre de décision qu'on prend mal à l'instinct et bien avec des chiffres.

Le tout sans abonnement, sans cookie tiers, et sans passer une journée à configurer un outil.

Tags :#Next.js#Vercel Analytics#Analytics#Performance#Développement web

Besoin d'aide ?

Confiez votre projet à un expert web en Valais

Partager cet article

Articles Similaires

4 mai 20267 min

Headers de sécurité HTTP sur Next.js : comment j'ai fait passer mon site de F à A

X-Frame-Options, CSP, HSTS, Permissions-Policy — comment configurer les headers de sécurité HTTP sur Next.js en 2026 et vérifier le résultat sur securityheaders.com.

#Sécurité#Next.js#HTTP Headers
4 mai 20268 min

WordPress vs Next.js en 2026 : lequel choisir pour votre site ?

WordPress ou Next.js ? Comparaison complète en 2026 : performance, SEO, coût, facilité d'utilisation. Quel CMS choisir pour votre projet en Suisse ?

#WordPress#Next.js#CMS
9 mars 20267 min

Next.js 16.2 vient de sortir : ce que ça change concrètement pour vos projets

87% de démarrage plus rapide, rendu serveur jusqu'à 60% plus rapide, logs des Server Functions, meilleur debug des erreurs d'hydratation. Tour d'horizon des nouveautés Next.js 16.2 qui changent le quotidien.

#Next.js#Turbopack#Performance