la privacy è un elenco, non un aggettivo: cosa invia davvero questo sito

dev · Oct 4, 2026 · 7 min read

stavo per scrivere che questo sito non invia niente da nessuna parte. niente analytics, niente tracker, niente cookie, tutto in prima parte. la frase era già scritta nella mia testa e sembrava fantastica.

poi ho aperto il pannello di rete e ho contato, perché un'affermazione come quella è vera o è falsa e non esiste una terza opzione.

**ha inviato tre richieste verso tre origini diverse a un caricamento a freddo, e il sorgente ne contiene altre due che consegnano il mio indirizzo ip a uno sconosciuto.**

## cosa esce davvero dal browser

questo è il registro di rete di un caricamento a freddo della homepage, completo:

| destinazione | cos'è | cosa imparano su di me |
| --- | --- | --- |
| `fonts.googleapis.com` | css per **JetBrains Mono** | che sono su questo sito |
| `fonts.gstatic.com` | il woff2 di JetBrains Mono | idem |
| `api.open-meteo.com` | il widget del meteo | **la mia latitudine e longitudine, nella query string** |

  c'è una quarta riga, ed è l'unica che ho scelto di mettere lì:

  | destinazione | cos'è | cosa imparano su di me |
  | --- | --- | --- |
  | `www.googletagmanager.com` | Google Analytics 4 | **che ho visitato una pagina qui, e quando** |

  quella è vera, l'ho aggiunta apposta, e non parte a meno che tu non prema prima un pulsante. `src/lib/analytics.ts` tiene la risposta in `localStorage`, e `gtag.js` viene iniettato solo dopo un `granted`. rifiuti, e lo script non viene mai scaricato e un `no` resta ricordato; ricompare a ogni visita e qualcuno lo legge come pressione, quindi viene chiesto una volta sola. **prima che tu scelga, questa tabella è di tre righe, non quattro.**

  di quella ultima riga devo parlare. la richiesta completa era:

```
/v1/forecast?latitude=44.4949&longitude=11.3426&current=temperature_2m,...
```

le mie coordinate, in chiaro, dentro una url, per mostrarmi la temperatura. ed ecco da dove sono arrivate — o meglio, da dove possono arrivare, perché su questo caricamento preciso non servivano. `detectUserLocation()` in `HomePage.tsx` prova `get.geojs.io/v1/ip/geo.json` e, se fallisce, `ipwho.is`. **entrambi sono servizi di geolocalizzazione via ip**, il che significa che entrambi ricevono il tuo indirizzo ip come prezzo di un codice postale. non sono partiti qui perché il widget aveva già una posizione; quando non ce l'ha, l'ip di un visitatore finisce in sequenza presso due sconosciuti, solo per saltare un menu a tendina.

il conteggio onesto è quindi di tre richieste a freddo e di cinque endpoint nel sorgente, e quattro di questi esistono per servire un singolo widget. l'ho costruito senza pensarci un attimo.

ciò che *non* c'è è ancora la parte più interessante, anche dopo l'analytics. **nessun tag manager.** nessun crash reporting. nessun session replay. nessun pixel, nessun beacon, nessun `document.cookie` da nessuna parte nel sorgente. le impostazioni stanno in `localStorage` e non vanno da nessuna parte. l'app, gli stili, le immagini, i due font dell'interfaccia: tutto dal mio stesso dominio, un solo hostname, nessuna terza parte.

  quest'ultima frase vale la pena difenderla per bene, perché «nessuna terza parte» non è vero e non lo è mai stato — la tabella qui sopra è lunga tre righe e ognuna è il server di qualcun altro. ciò che è vero è più stretto: **l'app in sé non parla mai con nessuno.** un'origine serve il codice, gli stili e le immagini. le terze parti le chiama il browser, dalla pagina, e le due che contano si possono spegnere senza toccare una riga dell'app.

  e la riga dell'analytics è il tipo onesto di aggiunta. avrei potuto scrivere questo articolo tra un anno e lasciare che la frase in cima marcisse lentamente, ed è la mossa che non ho fatto, perché un articolo sulla privacy che invecchia è peggio di nessun articolo sulla privacy: insegna alle persone la lezione sbagliata sul valore di controllare.

## la parte che è davvero buona, e che nessuno fotografa

**la favicon costa zero richieste.** questo sito ridisegna la sua icona di scheda a runtime, nel colore del tema corrente, dal tracciato del logo sigma incorporato nel bundle:

```tsx
const href = `data:image/svg+xml;utf8,${encodeURIComponent(buildSigmaFavicon(color))}`;
document.querySelectorAll<HTMLLinkElement>('link[rel~="icon"]').forEach((link) => {
  link.href = href;
});
```

cambi l'accento e l'icona della scheda segue, perché è una data uri, quindi non c'è nulla da scaricare. **nel registro di rete non c'è proprio nessuna voce `/favicon.ico`.** l'icona `.ico` statica, l'apple-touch-icon e i png del manifesto ci sono ancora, e devono esserci: sono quello che ottieni prima che react monti e sui client che non riescono a eseguire javascript o a rendere gli svg. l'ultimo `<link>` vince dove sono supportati entrambi, quindi quello vettoriale va per ultimo.

è una cosa piccola, ed è l'esempio più chiaro di privacy-by-construction che ho nella codebase: la funzionalità non è «non ti tracciamo», è «questa richiesta specifica non esiste».

**nessuna sourcemap.** una riga in `vite.config.ts`:

```ts
build: {
  sourcemap: false,
}
```

una sourcemap è una mappa completa del tuo sorgente, consegnata a chiunque carica la pagina, di solito per sbaglio perché una volta era utile nei devtools. è l'impostazione più privata di un bundler e la più facile da dimenticare. `dist/` non contiene nessun file `.map`.

## i nomi di file con hash non sono privacy

ogni asset di questo sito ha un hash del contenuto nel nome: `a/` e `c/` per i chunk js, `s/` per il css. deliberatamente non ne incollo qui uno, perché cambia a ogni build e questo paragrafo sarebbe sbagliato domani.

la gente elenca i nomi di file con hash tra gli articoli sulla privacy. non è privacy, è caching: l'hash c'è perché un deploy non invalidi un anno di cache del browser. l'hash non ha niente a che fare con l'indirizzo di nessuno.

lo scrivo qui perché confondere le due cose è esattamente ciò che rende inutili gli articoli «sito sulla privacy». un bundle con un hash di contenuto nel nome non è privato. uno script di terze parti da 10 MB non è privato comunque, che il suo nome abbia un hash o no.

per quello che vale, «dimensione» vuol dire tre numeri diversi per lo stesso entry chunk: il file su disco, quello che manderebbe un server gzip, e quello che brotli mette davvero in rete. quest'ultimo arriva a poco più di un quarto del primo. non incollo qui conteggi esatti di byte, e il motivo è imbarazzante: **questo articolo è dentro il bundle che sta descrivendo**, quindi ogni numero che scrivo cambia il numero, e il paragrafo sarebbe sbagliato al prossimo deploy. un entry chunk, con hash, e circa un quarto della sua dimensione su disco in rete.

## il problema di jetbrains mono, che è imbarazzante

tutto quanto detto sopra sull'ospitare i font da soli è vero, e poi c'è questo, alla riga 1 di `src/index.css`:

```
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap");
```

e la variabile subito sotto:

```
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
```

due problemi, e il secondo è quello interessante.

il primo è che ogni altro carattere tipografico di questo sito è servito dalla mia stessa origine — `GoogleSansFlex-Variable.woff2` e `MaterialSymbolsRounded.woff2`, entrambi in `public/fonts/`, entrambi precaricati — e un `@import` rimette google nella stanza. il browser di un visitatore ora dice a google quale pagina sta guardando e più o meno da dove arriva, in cambio di un font monospace che la maggior parte di loro non vedrà mai renderizzato.

il secondo è che **il font è dietro un'impostazione**. c'è un interruttore «developer font» nella finestra delle impostazioni che porta tutta l'interfaccia su JetBrains Mono. quasi nessuno lo attiva. e l'`@import` non se ne cura: un `@import` css in cima a un foglio di stile è incondizionato, quindi viene scaricato a ogni caricamento a freddo che tu apra o no quell'interruttore. il percorso di codice rilevante per la privacy e il percorso di codice mostrato all'utente non sono lo stesso percorso, e quello comodo è quello che gira sempre.

la correzione è una riga. lo stack ha già dietro `ui-monospace`, `SFMono-Regular` e `monospace` — tre font che costano zero richieste — quindi cancellare l'import non degrada niente tranne le forme dei glifi.

## i font che spedisco e nessuno carica

questo non è privacy, è onestà sul mio stesso repo:

| file | byte | referenziato da |
| --- | --- | --- |
| `MaterialSymbolsRounded.woff2` | 5,345,304 | `index.html`, `index.css` |
| `GoogleSansFlex-Variable.woff2` | 1,945,520 | `index.html` |
| `RobotoFlex-Variable.ttf` | 1,787,292 | **niente** |
| `GoogleSansFlex-Latin.woff2` | 20,076 | **niente** |

9,098,192 byte in `dist/fonts/`, di cui 1,807,368 che nessuna pagina chiede mai. sono in `public/`, quindi vengono copiati in ogni deploy, sono scaricabili da chiunque indovini la url, e tra tre anni saranno ancora lì perché niente ti avvisa di un file che non viene mai richiesto.

e quello da 20.076 byte è imbarazzante in un modo preciso: **è il sottoinsieme latino da 20 KB di cui ho scritto un articolo intero.** l'ho misurato, ho spedito il font variabile da 1.945.520 byte invece perché ho deciso che quello completo fosse la scelta giusta, e poi non ho mai cancellato il sottoinsieme. il file è sopravvissuto alla decisione che l'aveva annullato. il numero nel mio articolo è ancora corretto e il file è ancora inutilizzato.

## cosa «la privacy come scelta di design» deve significare

non un disclaimer. non un banner dei cookie senza nulla da acconsentire. il test è una domanda:

**riesci a dire, senza girarci attorno, cosa esce dal browser?**

se non riesci, hai un'informativa sulla privacy. un'informativa è un documento che hai scritto una volta. se riesci — e l'elenco è corto, e ogni voce è una funzionalità che hai scelto apposta — allora hai una decisione di design. il mio sono quattro richieste, e da oggi posso nominare tutte e quattro e decidere su ognuna.

tutta la disciplina sta qui. non «rispettiamo la tua privacy». un elenco, verificato contro il pannello di rete, con dentro le coordinate.

## cosa cambierei, in quest'ordine

**1. cancellare l'`@import`.** lo stack di fallback è già lì. una riga, rimuove due origini di terze parti, non cambia nulla di visibile a meno che qualcuno abbia attivo un'impostazione che quasi nessuno tocca.

**2. mettere il meteo dietro un click.** questo è il fallimento vero, ed è mio, non loro: per risparmiarmi e risparmiare al visitatore un'interazione con un menu a tendina, due aziende imparano dove si trova il visitatore e una terza viene avvisata. la geolocalizzazione dovrebbe avvenire dopo un click sul widget meteo, mai prima, e mai per impostazione predefinita. se il widget non viene cliccato, nessuno dovrebbe essere localizzato.

**3. cancellare i due font morti.** 1,8 MB che nessuna pagina richiede, in ogni deploy, per sempre.

## cosa direi al mio io di passato

* **un'affermazione sulla privacy è un elenco con dei nomi dentro.** «non invia niente» è un aggettivo. «non invia niente tranne queste quattro cose, e le vedo tutte e quattro nel pannello» è un fatto su cui puoi essere preso in giro, ed è il punto.
* **un `@import` non è un'impostazione.** credevo di aver reso il font monospace opzionale. avevo reso *visibile* o no il font, che non è la stessa cosa, e la richiesta di rete c'era comunque. la funzionalità e la privacy sono percorsi di codice diversi, e solo uno dei due è dietro l'interruttore.
* **i numeri nei tuoi articoli invecchiano più in fretta dei tuoi bundle.** il sottoinsieme da 20 KB è ancora in questo repo, inutilizzato, della dimensione esatta di cui ho scritto mentre il file da 1,9 MB viene spedito. se mai correggi un numero, controlla se il file che descriveva esiste ancora — o cancellalo mentre ci sei.