Aller au contenu
Me contacter

Ouvert à un CDI · Métropole lilloise

Tous les articles
7 min de lecture

Consentement cookies RGPD avec Next.js : de la CNIL au code

Ce que la CNIL exige d’un bandeau de cookies, et comment le traduire en code : configuration par le back-office, preuves, modale accessible avec l’élément dialog et scripts chargés après le choix. À partir du module de consentement de la DXP Goodbizz.

  • Next.js
  • React
  • RGPD
  • CNIL
  • Cookies
  • Accessibilité

Dans Goodbizz, une DXP multi-tenants développée chez Ikomobi, chaque client publie plusieurs sites, applications et blogs depuis un seul back-office. J’y ai repris et fait évoluer le module de consentement, du modèle de données de l’API jusqu’à la modale affichée sur les sites Next.js, pour que chaque canal ait son propre bandeau, paramétré sans passer par un développeur. Un bandeau de cookies a l’air d’un composant comme un autre. Il porte pourtant des obligations précises, et la plupart se traduisent directement en choix de code. L’article part de ce que demande la CNIL, puis descend jusqu’à l’implémentation.

Que demande la CNIL, concrètement ?

Le cadre vient de l’article 82 de la loi Informatique et Libertés, qui transpose la directive ePrivacy, et la CNIL l’a précisé dans ses lignes directrices et sa recommandation de 2020. Pour un développeur, il se résume en quelques règles :

  • aucun traceur soumis au consentement n’est déposé ni lu tant que l’utilisateur n’a pas fait son choix ;
  • accepter et refuser demandent le même effort, par exemple avec un bouton « Tout refuser » au même niveau et avec le même aspect que « Tout accepter », de préférence sur le même écran ;
  • le choix se fait par finalité (mesure d’audience, publicité, contenus tiers…), chacune décrite ;
  • retirer son consentement doit être aussi simple que le donner, et possible à tout moment ;
  • le choix est conservé un temps raisonnable, que la CNIL situe à 6 mois comme bonne pratique, à apprécier selon le site.

Certains traceurs échappent au consentement : celui qui mémorise le choix lui-même, l’authentification, le panier d’achat, l’équilibrage de charge ou certaines mesures d’audience configurées en conséquence. Ce sont eux qui forment les catégories obligatoires d’un bandeau, affichées mais non désactivables.

Pourquoi rendre le bandeau configurable par le back-office ?

Sur une DXP, un même code sert des sites très différents. Un site vitrine peut n’avoir aucun traceur, une boutique un outil d’analyse et un pixel publicitaire, un blog des vidéos intégrées. Coder le bandeau en dur obligerait à livrer une nouvelle version à chaque nouvel outil marketing.

Le bandeau de chaque tenant est donc décrit en base, dans MongoDB, et se modifie depuis le back-office :

  • les catégories, chacune avec sa description, son caractère obligatoire et la liste de ses traceurs ;
  • les textes et les libellés des boutons, en français et en anglais, et une image d’en-tête ;
  • la durée de validité du choix, exprimée en jours, en semaines ou en mois ;
  • la politique de confidentialité, rédigée dans le back-office ou remplacée par un lien vers une page existante.
// Simplified Mongoose schema
const Category = new Schema({
  key: String, // "analytics", "ads"...
  title: String,
  content: String,
  required: Boolean, // exempt trackers: shown, not switchable
  trackers: [{ key: String, title: String, content: String }],
});

const ConsentConfig = new Schema(
  {
    tenantId: { type: String, required: true },
    active: { type: Boolean, default: true },
    validity: { number: Number, unit: { type: String, enum: ["day", "week", "month"] } },
    translations: [{ languageCode: String, descriptionShort: String, categories: [Category] }],
  },
  { timestamps: true },
);

Une route publique de l’API renvoie ensuite la version active dans la langue demandée, avec la durée convertie en secondes et la politique de confidentialité, prête à être affichée.

Comment prouver ce qui a été affiché ?

Le RGPD demande de pouvoir prouver le consentement. Parmi les moyens que cite la CNIL figurent des captures d’écran horodatées de chaque version du site et la conservation horodatée des configurations de l’outil de recueil.

Le module couvre ces deux points. Chaque modification de la configuration crée une nouvelle version et désactive la précédente au lieu de l’écraser, si bien que l’on sait à tout moment quel bandeau était en ligne à une date donnée. Le back-office permet aussi de déposer des captures du bandeau, titrées et datées, rangées par tenant.

// Simplified example: a change never overwrites the live version
async function saveConfig(tenantId, data) {
  await ConsentConfig.updateOne({ tenantId, active: true }, { active: false });
  return ConsentConfig.create({ ...data, tenantId, active: true });
}

Deux compléments renforcent cette preuve. Enregistrer dans le cookie l’identifiant de la version affichée permet de redemander le choix quand la configuration change, à l’ajout d’un nouvel outil par exemple. Et pour les sites qui en ont besoin, un journal des choix côté serveur (date, version, catégories acceptées, sans donnée identifiante superflue) documente chaque consentement individuellement.

Comment construire la modale avec Next.js ?

Le layout racine, un Server Component, récupère la configuration active du tenant et la transmet à un composant client. Celui-ci lit le cookie de choix et n’affiche la modale que s’il est absent, de sorte qu’un visiteur qui a déjà répondu ne la revoit pas pendant toute la durée de validité.

La modale est un élément dialog natif, ouvert avec showModal(). Le navigateur fournit alors ce qu’il faudrait sinon coder et tester à la main : le focus placé dans la fenêtre, le reste de la page rendu inerte, le rôle de fenêtre modale annoncé aux lecteurs d’écran et la fermeture par la touche Échap. L’article sur l’accessibilité revient sur ces bases.

Les trois boutons du premier écran, Personnaliser, Tout refuser et Tout accepter, partagent exactement le même style, comme le demande la CNIL. Dans la vue détaillée, les catégories obligatoires apparaissent avec leur interrupteur verrouillé, et « Tout refuser » ne conserve qu’elles.

"use client";

export function ConsentDialog({ config, onChoice }) {
  const ref = useRef(null);

  useEffect(() => {
    if (!readChoice()) ref.current?.showModal();
  }, []);

  const refuseAll = () => onChoice(onlyRequired(config.categories));
  const acceptAll = () => onChoice(allCategories(config.categories));

  return (
    // Escape fires "cancel": treated as a refusal
    <dialog ref={ref} aria-labelledby="consent-title" onCancel={refuseAll}>
      <h2 id="consent-title">{config.title}</h2>
      {/* description and privacy policy link */}
      <button className="consent-button" onClick={openDetails}>Personnaliser</button>
      <button className="consent-button" onClick={refuseAll}>Tout refuser</button>
      <button className="consent-button" onClick={acceptAll}>Tout accepter</button>
    </dialog>
  );
}

La touche Échap mérite une attention particulière. Elle ferme la fenêtre sans qu’aucun bouton ne soit cliqué, et sans traitement aucun choix n’est enregistré, si bien que le bandeau revient à la page suivante. L’événement cancel permet de trancher, soit en l’interprétant comme un refus, ce que la CNIL admet tant que l’utilisateur en est informé, soit en l’empêchant pour que les boutons restent la seule sortie.

Le choix lui-même est stocké dans un cookie propre au site, exempté de consentement puisqu’il ne sert qu’à mémoriser la décision. Sa durée correspond à la validité configurée, avec SameSite=Lax et l’attribut Secure en HTTPS.

Comment bloquer les scripts tant que l’utilisateur n’a pas choisi ?

Un bandeau qui enregistre un refus mais laisse le script d’analyse se charger ne protège personne. La règle est donc de ne rendre un script tiers qu’une fois sa catégorie acceptée. Avec Next.js, le composant Script se place dans un composant client qui lit le choix :

"use client";
import Script from "next/script";

export function AnalyticsScripts() {
  const choice = useConsent(); // reads the cookie, updated on each new choice
  if (!choice?.analytics) return null;

  return <Script src="https://analytics.example.com/script.js" strategy="afterInteractive" />;
}

Le hook doit aussi réagir à un changement de choix pendant la visite. Un événement émis à l’enregistrement du cookie suffit, sans recharger la page. Les contenus intégrés (vidéos YouTube, cartes) suivent la même logique, avec un aperçu statique tant que leur catégorie n’est pas acceptée.

Pour Google Analytics ou Google Ads, Google propose aussi le Consent Mode : les balises démarrent avec ad_storage, analytics_storage, ad_user_data et ad_personalization à denied, puis une commande update reprend le choix de l’utilisateur. Ne charger les balises qu’après accord reste toutefois, à mon sens, la solution la plus simple à vérifier.

Comment laisser l’utilisateur changer d’avis ?

Retirer son consentement doit être aussi simple que le donner. En pratique, un lien « Gérer mes cookies » dans le pied de page rouvre la modale sur la vue détaillée, avec les choix en cours déjà cochés, puisque le composant lit le cookie pour initialiser ses interrupteurs.

Retirer un consentement ne supprime pas pour autant les cookies déjà déposés par un outil tiers. Le code qui enregistre le nouveau choix doit donc aussi effacer ceux du domaine (_ga pour Google Analytics, par exemple) et ne plus charger le script à la navigation suivante.

Enfin, le choix expire. À la fin de sa durée de validité, le cookie disparaît et le bandeau réapparaît, ce qui est le comportement attendu. Les 6 mois cités par la CNIL sont un bon point de départ.