la vie privée est une liste, pas un adjectif : ce que ce site envoie vraiment
dev · Oct 4, 2026 · 7 min read
j'allais écrire que ce site n'envoie rien nulle part. pas de statistiques, pas de traceurs, pas de cookies, tout en first-party. la phrase était déjà écrite dans ma tête et elle sonnait bien.
puis j'ai ouvert le panneau réseau et j'ai compté, parce qu'une affirmation comme celle-là est soit vraie, soit un mensonge, et il n'y a pas de troisième option.
**il a envoyé trois requêtes vers trois origines différentes lors d'un chargement à froid, et le code source en contient deux autres qui transmettent mon adresse ip à un inconnu.**
## ce qui sort réellement du navigateur
voici le journal réseau d'un chargement à froid de la page d'accueil, complet :
| destination | ce que c'est | ce qu'ils apprennent de moi |
| --- | --- | --- |
| `fonts.googleapis.com` | le css de **JetBrains Mono** | que je suis sur ce site |
| `fonts.gstatic.com` | le woff2 de JetBrains Mono | idem |
| `api.open-meteo.com` | le widget météo | **ma latitude et ma longitude, dans la chaîne de requête** |
il y a une quatrième ligne, et c'est la seule que j'aie choisi d'y mettre :
| destination | ce que c'est | ce qu'ils apprennent de moi |
| --- | --- | --- |
| `www.googletagmanager.com` | Google Analytics 4 | **que j'ai visité une page ici, et quand** |
celle-là est réelle, je l'ai ajoutée exprès, et elle ne se déclenche pas tant que vous n'avez pas d'abord cliqué un bouton. `src/lib/analytics.ts` garde la réponse dans `localStorage`, et `gtag.js` n'est injecté qu'après un `granted`. refusez, et le script n'est jamais récupéré et un `no` est mémorisé ; le redemander à chaque visite, et certains y lisent de la pression, donc on demande une seule fois. **avant que vous ne choisissiez, ce tableau fait trois lignes, pas quatre.**
c'est de cette dernière ligne que je dois parler. la requête complète était :
```
/v1/forecast?latitude=44.4949&longitude=11.3426¤t=temperature_2m,...
```
mes coordonnées, en clair, dans une url, pour m'afficher la température. et voici d'où elles viennent — ou plutôt d'où elles *peuvent* venir, car sur ce chargement précis elles n'en avaient pas besoin. `detectUserLocation()` dans `HomePage.tsx` essaie `get.geojs.io/v1/ip/geo.json` et, en cas d'échec, `ipwho.is`. **ce sont deux services de géolocalisation par ip**, ce qui signifie que les deux reçoivent votre adresse ip comme prix d'un code postal. ils ne se sont pas déclenchés ici parce que le widget avait déjà une position ; lorsqu'il n'en a pas, l'ip d'un visiteur part chez deux inconnus l'un après l'autre, simplement pour éviter un menu déroulant.
le compte honnête est donc de trois requêtes à froid et de cinq points d'accès dans le code, dont quatre existent pour servir un seul widget. je l'ai construit sans y penser une seconde.
ce qui *n'y est pas* reste la partie la plus intéressante, même après les statistiques. **aucun tag manager.** aucun rapport d'erreurs. aucune relecture de session. aucun pixel, aucun beacon, aucun `document.cookie` nulle part dans le code source. les réglages vivent dans `localStorage` et ne vont nulle part. l'application, les styles, les images, les deux polices d'interface : tout vient de ma propre origine, un seul nom d'hôte, aucun tiers.
cette dernière phrase mérite d'être défendue correctement, car « aucun tiers » n'est pas vrai et ne l'a jamais été — le tableau ci-dessus fait trois lignes et chacune est le serveur de quelqu'un d'autre. ce qui est vrai est plus étroit : **l'application elle-même ne parle à personne.** une origine sert le code, les styles et les images. les tiers sont appelés par le navigateur, depuis la page, et les deux qui comptent peuvent être coupés sans toucher une ligne de l'application.
et la ligne des statistiques est l'ajout honnête. j'aurais pu écrire ce billet dans un an et laisser la phrase du haut se décomposer en silence, et c'est le coup que je n'ai pas fait, parce qu'un billet sur la vie privée qui vieillit est pire qu'aucun billet sur la vie privée : il enseigne aux gens la mauvaise leçon sur la valeur de vérifier.
## la partie qui est vraiment bonne, et que personne ne met en capture d'écran
**le favicon coûte zéro requête.** ce site redessine son icône d'onglet à l'exécution, dans la couleur du thème courant, à partir du tracé du logo sigma embarqué dans le bundle :
```tsx
const href = `data:image/svg+xml;utf8,${encodeURIComponent(buildSigmaFavicon(color))}`;
document.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]').forEach((link) => {
link.href = href;
});
```
changez la couleur d'accent et l'icône de l'onglet suit, parce que c'est une data-uri : il n'y a donc rien à récupérer. **le journal réseau ne contient aucune entrée `/favicon.ico` du tout.** l'icône `.ico` statique, l'apple-touch-icon et les png du manifeste sont toujours là, et ils doivent l'être : c'est ce que vous obtenez avant que react ne monte, et sur les clients qui ne peuvent pas exécuter de javascript ou rendre du svg. le dernier `<link>` gagne là où les deux sont pris en charge, donc le vectoriel passe en dernier.
c'est une petite chose, et c'est l'exemple le plus net de privacy-by-construction que j'aie dans le code : la fonctionnalité n'est pas « nous ne vous suivons pas », c'est « cette requête précise n'existe pas ».
**aucune sourcemap.** une ligne dans `vite.config.ts` :
```ts
build: {
sourcemap: false,
}
```
une sourcemap est une carte complète de votre code source, livrée à quiconque charge la page, généralement par accident parce qu'elle a été utile une fois dans les devtools. c'est le réglage le plus privé d'un bundler et le plus facile d'oublier. `dist/` ne contient aucun fichier `.map`.
## les noms de fichiers hachés ne sont pas de la vie privée
chaque asset de ce site a un hash de contenu dans son nom : `a/` et `c/` pour les chunks js, `s/` pour le css. je ne colle délibérément aucun ici, parce qu'il change à chaque build et que ce paragraphe serait faux demain.
les gens rangent les noms de fichiers hachés dans les billets sur la vie privée. ce n'est pas de la vie privée, c'est du cache : le hash est là pour qu'un déploiement n'invalide pas un an de cache navigateur. le hash n'a rien à voir avec l'adresse de qui que ce soit.
je l'écris ici parce que confondre les deux est précisément ce qui rend inutiles les billets « site de vie privée ». un bundle avec un hash de contenu dans le nom n'est pas privé. un script tiers de 10 mo n'est pas privé non plus, que son url contienne ou non un hash.
pour ce que ça vaut, « taille » veut dire trois nombres différents pour le même entry chunk : le fichier sur disque, ce qu'un serveur gzip enverrait, et ce que brotli met effectivement sur le réseau. ce dernier représente environ un quart du premier. je ne colle pas ici de comptes d'octets exacts, et la raison est embarrassante : **ce billet est à l'intérieur du bundle qu'il décrit**, donc chaque nombre que j'écris change le nombre, et le paragraphe serait faux au prochain déploiement. un entry chunk, haché, et environ un quart de sa taille sur disque sur le réseau.
## le problème de jetbrains mono, qui est embarrassant
tout ce qui précède sur l'hébergement de ses propres polices est vrai, et puis il y a ceci, ligne 1 de `src/index.css` :
```
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap");
```
et la variable juste en dessous :
```
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
```
deux problèmes, et le deuxième est le plus intéressant.
le premier : chaque autre police de ce site est servie depuis ma propre origine — `GoogleSansFlex-Variable.woff2` et `MaterialSymbolsRounded.woff2`, toutes deux dans `public/fonts/`, toutes deux préchargées — et un seul `@import` ramène google dans la pièce. le navigateur d'un visiteur dit maintenant à google quelle page il regarde et à peu près d'où il vient, en échange d'une police à chasse fixe que la plupart ne verront jamais rendue.
le deuxième : **la police est derrière un réglage.** il y a un interrupteur « developer font » dans la boîte de dialogue des réglages qui bascule toute l'interface sur JetBrains Mono. presque personne ne l'active. et l'`@import` s'en fiche : un `@import` css en haut d'une feuille de style est inconditionnel, donc il est récupéré à chaque chargement à froid, que vous ouvriez jamais cet interrupteur ou non. le chemin de code lié à la vie privée et le chemin de code visible par l'utilisateur ne sont pas le même chemin, et c'est celui du confort qui s'exécute toujours.
la correction tient en une ligne. la pile contient déjà `ui-monospace`, `SFMono-Regular` et `monospace` derrière elle — trois polices qui coûtent zéro requête — donc supprimer l'import ne dégrade rien, à part la forme des glyphes.
## les polices que je livre et que personne ne charge
ce n'est pas de la vie privée, c'est de l'honnêteté sur mon propre dépôt :
| fichier | octets | référencé par |
| --- | --- | --- |
| `MaterialSymbolsRounded.woff2` | 5,345,304 | `index.html`, `index.css` |
| `GoogleSansFlex-Variable.woff2` | 1,945,520 | `index.html` |
| `RobotoFlex-Variable.ttf` | 1,787,292 | **rien** |
| `GoogleSansFlex-Latin.woff2` | 20,076 | **rien** |
9.098.192 octets dans `dist/fonts/`, dont 1.807.368 que aucune page ne demande jamais. ils sont dans `public/`, donc copiés dans chaque déploiement, récupérables par quiconque devine l'url, et ils seront encore là dans trois ans parce que rien ne vous prévient d'un fichier qui n'est jamais demandé.
et celui de 20.076 octets est embarrassant d'une manière précise : **c'est le sous-ensemble latin de 20 ko dont j'ai écrit un billet entier.** je l'ai mesuré, j'ai livré la police variable de 1.945.520 octets à la place parce que j'avais décidé que la complète était le bon choix, et puis je n'ai jamais supprimé le sous-ensemble. le fichier a survécu à la décision qui l'annulait. le nombre dans mon billet est toujours correct et le fichier est toujours inutilisé.
## ce que « la vie privée comme choix de design » doit vouloir dire
pas une clause de non-responsabilité. pas une bannière de cookies sans rien à accepter. le test tient en une question :
**pouvez-vous dire, sans tourner autour, ce qui sort du navigateur ?**
si vous ne pouvez pas, vous avez une politique de confidentialité. une politique, c'est un document que vous avez écrit une fois. si vous pouvez — et la liste est courte, et chaque entrée est une fonctionnalité que vous avez choisie exprès — alors vous avez une décision de design. la mienne, ce sont quatre requêtes, et à partir d'aujourd'hui je peux nommer les quatre et décider pour chacune.
c'est toute la discipline. pas « nous respectons votre vie privée ». une liste, vérifiée contre le panneau réseau, coordonnées comprises.
## ce que je changerais, dans cet ordre
**1. supprimer l'`@import`.** la pile de repli est déjà là. une ligne, retire deux origines tierces, ne change rien de visible sauf pour ceux qui ont activé un réglage que presque personne ne touche.
**2. mettre la météo derrière un clic.** c'est le vrai échec, et il est le mien, pas le leur : pour m'épargner et épargner au visiteur une interaction de menu déroulant, deux entreprises apprennent où se trouve le visiteur et une troisième est informée. la géolocalisation devrait avoir lieu après un clic sur le widget météo, jamais avant, et jamais par défaut. si le widget n'est pas cliqué, personne ne devrait être localisé.
**3. supprimer les deux polices mortes.** 1,8 mo que aucune page ne demande, dans chaque déploiement, pour toujours.
## ce que je dirais à mon moi d'avant
* **une affirmation sur la vie privée est une liste avec des noms dedans.** « n'envoie rien » est un adjectif. « n'envoie rien sauf ces quatre choses, et je vois les quatre dans le panneau » est un fait dont on peut vous prendre, et c'est précisément le but.
* **un `@import` n'est pas un réglage.** je croyais avoir rendu la police à chasse fixe facultative. j'avais rendu la police *visible* ou non, ce qui n'est pas la même chose, et la requête réseau était là dans les deux cas. la fonctionnalité et la vie privée sont deux chemins de code différents, et un seul est derrière l'interrupteur.
* **les nombres dans vos billets vieillissent plus vite que vos bundles.** le sous-ensemble de 20 ko est toujours assis dans ce dépôt, inutilisé, de la taille exacte dont j'ai écrit, pendant que le fichier de 1,9 mo est livré. si vous corrigez un jour un nombre, vérifiez si le fichier qu'il décrivait existe encore — ou supprimez-le pendant que vous y êtes.