Verificare le fonti di un testo AI con uno script Node
Uno script Node da 144 righe scarica ogni fonte citata da un agente AI e cerca la frase esatta nella pagina. Il codice, i passi e i limiti.

Indice dei contenuti
Gli articoli di questo blog li scrive un agente AI e li pubblica una routine con un push diretto su main, che su Vercel equivale a un deploy (ne ho parlato a proposito del middleware tolto per i costi), senza che nessun umano li legga prima. Il rischio vero non è l’errore di grammatica: è il fatto inventato scritto in italiano perfetto, che nessun controllo di stile può vedere. Qui trovi lo script Node che uso per verificare le fonti di un testo AI prima che vada online: le parti del codice che contano, i casi in cui ha bloccato e quello in cui non poteva bloccare.
Il ristorante che non esisteva
Il 27 settembre 2026 ho aggiunto al repo scripts/check-sources.mjs. Il motivo sta nel messaggio di quel commit (cb4c155): la PR #60 apriva con un ristorante di San Francisco che nessuna fonte cita, ed era stata mergiata. Nello stesso commit, da quell’articolo sono uscite due affermazioni che nessun sito confermava.
Fino a quel giorno l’unico filtro sui testi era check:ai, che cerca i tic lessicali dei modelli: aggettivi gonfiati, chiusure da tema scolastico, em-dash. Funziona, ma misura come è scritto un testo, non se è vero. Un modello inventa con la stessa sicurezza con cui riporta, e il dettaglio inventato non porta nessun segno esteriore che lo tradisca.
L’idea è semplice: se un fatto ha una fonte, quel documento deve esistere e deve dire quella cosa. Entrambe le condizioni le controlla una macchina in pochi secondi.
Il contratto: url e frase copiata
Ogni articolo dichiara le sue fonti nel frontmatter, una voce per indirizzo, con accanto un brano copiato parola per parola, nella lingua originale:
sources:
- url: "https://nodejs.org/api/globals.html"
quote: "Returns a new AbortSignal which will be aborted in delay milliseconds."
È il frontmatter di questo articolo. La scelta della frase copiata, invece di un riassunto, è il punto su cui regge tutto: un riassunto si può scrivere senza aver letto niente, una citazione esatta di almeno 20 caratteri è molto più difficile da inventare. E se è inventata, il controllo se ne accorge, perché nel testo scaricato non la trova.
Le regole applicate sono quattro:
- ogni link esterno del corpo deve comparire in
sources; - ogni fonte di
sourcesdeve essere linkata nel corpo (che stia accanto al fatto lo chiedeAGENTS.md, lo script controlla solo che il link ci sia); - il server deve rispondere (un URL inventato di solito dà 404);
- la citazione deve stare nel testo scaricato.
Gli articoli più vecchi non hanno il campo, quindi il controllo parte da una data: SOURCES_REQUIRED_FROM = '2026-09-28'. Prima di quella data un articolo si controlla solo se dichiara sources. Un articolo di sola opinione può non averne, ma basta un link esterno nel testo e la regola 1 lo obbliga a dichiararlo.
Scaricare l’HTML senza restare appesi
Node 22 ha fetch globale, quindi non serve nessuna dipendenza per scaricare le pagine. Serve però un limite di tempo: un sito che non risponde non deve bloccare la pubblicazione per minuti. Ecco la funzione, così com’è in scripts/check-sources.mjs:
async function fetchText(url) {
const res = await fetch(url, {
redirect: 'follow',
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36',
'Accept-Language': 'it-IT,it;q=0.9,en;q=0.8',
},
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return pageText(await res.text())
}
AbortSignal.timeout() restituisce un segnale che, come dice la documentazione di Node, viene annullato dopo il numero di millisecondi indicato: qui 20.000. Niente setTimeout da ripulire a mano. Lo User-Agent da browser c’è perché alcuni siti rispondono in modo diverso a un client che si presenta come bot.
Nel ciclo principale c’è la distinzione che conta di più. Un 404 o un 410 diventano “la pagina non esiste”, qualunque altro errore “pagina non verificabile”. Sono due messaggi diversi, ma bloccano tutti e due. Un articolo dietro paywall o anti-bot non è per forza falso, però in un testo che esce senza revisione umana una fonte che nessuno può aprire vale quanto una inventata.
Confrontare testo e citazione
Il confronto non può essere un includes sull’HTML grezzo. Il documento scaricato contiene tag, entità (è, ’), apostrofi tipografici, spazi a capo. Il brano nel frontmatter l’ha trascritto un modello che magari ha uniformato le virgolette. Prima del confronto entrambi i testi passano dalla stessa trafila:
function normalize(text) {
return text
.replace(/&#x([0-9a-f]+);/gi, (_, h) => String.fromCodePoint(parseInt(h, 16)))
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)))
.replace(/&([a-z]+);/gi, (m, name) => ENTITIES[name.toLowerCase()] ?? m)
.normalize('NFC')
.replace(/[‘’‚′`]/g, "'")
.replace(/[“”„″«»]/g, '"')
.replace(/[–—]/g, '-')
.replace(/…/g, '...')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase()
}
La riga meno ovvia è .normalize('NFC'). Una “è” può arrivare come un solo carattere o come “e” più accento combinante: a occhio sono identiche, per includes no. NFC, spiega MDN, è la forma composta, quella in cui più code point diventano uno solo quando si può.
pageText toglie prima <script> e <style> con il loro contenuto, poi sostituisce ogni altro tag con uno spazio e passa il risultato a normalize. Il controllo finale è una riga: text.includes(normalize(quote)).
Come verificare che funziona
Il modo più rapido è passare un file alla volta:
npm run check:sources content/blog/verificare-citazioni-testi-agente-ai.mdx
L’uscita buona è ✓ fonti verificate (1 articoli con "sources")., con quel plurale sbagliato che non ho mai corretto. Per vedere un errore basta cambiare una parola della citazione: in console compaiono il numero della voce, l’URL e i primi 80 caratteri del brano mancante, poi esce con codice 1, e la routine non pusha.
Ho provato pageText e normalize anche da soli, copiandoli in un file fuori dal repo ed eseguendoli con Node 22.23. È lì che si vede il primo errore tipico.
Gli errori che ho incontrato
Il tag seguito da punteggiatura. Sostituire i tag con uno spazio evita di incollare due parole, ma aggiunge uno spazio prima di virgole e punti:
const text = pageText('<p>Usa <code>AbortSignal.timeout()</code>, poi riprova.</p>')
console.log(text)
// usa abortsignal.timeout() , poi riprova.
console.log(text.includes(normalize('Usa AbortSignal.timeout(), poi riprova.')))
// false
Il periodo è lì, eppure il confronto fallisce. Il rimedio che uso non è cambiare il codice: è scegliere citazioni che non attraversano un tag seguito da punteggiatura.
I siti che non si lasciano leggere. Nella memoria della routine (docs/ROUTINE-BLOG-MEMORIA.md) c’è un elenco che cresce: DDay.it risponde 403, EUR-Lex risponde 202 con una pagina vuota, whatsapp.com dà 400 a quello User-Agent, le FAQ di WhatsApp sono generate in JavaScript e l’HTML scaricato non contiene il testo. Per EUR-Lex dispiace, perché è lì che si leggono i testi delle norme europee. Ma la regola resta: riferimento non verificabile, riferimento da cambiare.
I blocchi temporanei. nic.it, dopo qualche richiesta ravvicinata, risponde 503 per alcuni minuti. Qui la risposta giusta è aspettare e rilanciare, non cercare un’alternativa: manca un meccanismo di retry, e per ora va bene così.
Quello che il confronto di stringhe non vede
C’è un errore che è passato lo stesso. Il 25 ottobre un articolo citava SmartWorld per la fine degli aggiornamenti estesi di Windows 10, il 12 ottobre 2027. Il periodo era davvero sul sito e la citazione in sources superava il test. Ma quella data vale per le aziende al secondo anno a pagamento, non per tutti: è stata corretta a mano lo stesso giorno (commit bfb3524), citando la documentazione di Microsoft.
Il controllo dimostra che la fonte contiene la citazione, non che l’articolo afferma ciò che la fonte afferma. Il salto tra “lì c’è scritto questo” e “quello che ho scritto io è vero” resta fuori portata, e non credo che un algoritmo così possa coprirlo.
Per quel salto oggi la routine fa rileggere l’articolo a un secondo agente, in un contesto pulito, che apre i link e segnala i passaggi che dicono più di quanto l’originale sostiene. Negli ultimi articoli ha sempre trovato qualcosa: un “negli Stati Uniti” che la fonte non diceva, una percentuale riferita a un caso più ampio del suo. Non è una garanzia nemmeno quello, perché è un modello che controlla un modello: lo stesso discorso che facevo sul vibe coding, dove scrivere codice costa poco e il mestiere diventa curare quello che esce. Ma i due controlli sbagliano in modi diversi, ed è per questo che li tengo tutti e due.
Se vuoi copiare qualcosa, parti dal contratto: un fatto senza un brano copiato da un indirizzo raggiungibile non si pubblica. Il codice sono 144 righe e viene dopo.
Chi sta valutando a chi affidare un progetto può aggiungere una domanda ai criteri per scegliere un web developer: come controlli quello che scrivono i tuoi agenti? E per un flusso di contenuti generati su cui vuoi un cancello simile, scrivimi.
Continua a leggere

Oltre la prima pagina: la SEO nell'era delle AI Overviews
Smetti di lottare per il primo posto e inizia a farti citare dall'AI. Benvenuti nell'era della GEO: la nuova frontiera della visibilità digitale.
7 min di lettura
Serve una firma hardware per dimostrare che siamo umani
Apple introduce la firma hardware sulle foto. Una riflessione su come i modelli generativi stiano imponendo la patente di autenticità online.
5 min di lettura
Quando a navigare il tuo sito non è più un essere umano
Cosa cambia per interfacce e sicurezza quando a esplorare i nostri siti non sono più le persone, ma gli agenti software autonomi che agiscono da soli.
6 min di lettura