TL;DR: un modello che scrive un articolo produce URL plausibili, non URL esistenti. Prima di promuovere una bozza a publish, passa tutti gli href con una richiesta HEAD e confronta la URL finale con quella di partenza. Sono trenta secondi e ti evitano di firmare un pezzo che cita fonti inesistenti.
Il problema: l’URL sbagliato è quello che sembra giusto
Quando chiedi a un modello di documentare un’affermazione, il link che ottieni ha la forma esatta di un link vero: dominio corretto, path verosimile, slug coerente col titolo. È questo che lo rende pericoloso. Un https://developer.wordpress.org/reference/functions/wp_check_remote_url/ non stona alla lettura: stona solo quando qualcuno ci clicca.
Chi rilegge la bozza controlla numeri e date, perché lì l’errore si vede. I link no: li si guarda, sembrano ragionevoli, si va avanti. Un refuso lo correggi dopo. Una fonte inesistente è credibilità persa, e la trova il lettore prima di te.
La soluzione: un controllo sulle bozze, non una rilettura
Il punto giusto è tra draft e publish, sullo stesso principio del gate che tiene in bozza i contenuti generati. Estrai gli href, chiedi a ogni URL una HEAD e stampa su una riga sola ID, URL di partenza, codice e URL di arrivo:
wp post list --post_type=post,page --post_status=draft --field=ID | while read -r id; do
wp post get "$id" --field=post_content \
| grep -oE "href=[\"']https?://[^\"']+" \
| sed -E "s/^href=.//; s/&(amp|#0*38);/\&/g" | sort -u \
| while read -r url; do
r=$(curl -sIL -m 10 -o /dev/null -w '%{http_code} %{url_effective}' "$url")
case "$r" in 403*|405*|501*)
r=$(curl -sL -m 10 -o /dev/null -w '%{http_code} %{url_effective}' "$url") ;;
esac
printf '%s %s %s\n' "$id" "$url" "$r"
done
done
Quattro dettagli che sembrano pignoleria e non lo sono. Gli ID si chiedono con --field=ID, uno per riga: --format=ids li stampa su una riga sola senza a capo finale, e read scarta l’ultima riga non terminata — con una bozza sola il ciclo non gira e l’output vuoto sembra un via libera. La classe di caratteri accetta sia " sia ', perché un href='...' saltato non produce una riga di errore: produce silenzio. Il sed rimette l’ampersand al posto di &, la forma in cui WordPress serve le query string. E il retry in GET scatta solo su 403, 405 e 501, i codici con cui un CDN o un WAF rifiuta la richiesta o il metodo: ritentare anche i 404 vuol dire coprire una risposta corretta con il 200 di una pagina d’errore.
L’errore da evitare: leggere solo il codice di stato
Molti siti non rispondono 404 su un path sconosciuto: fanno un 302 verso la home, che risponde 200. La catena è verde e la fonte non esiste. Per questo il comando stampa sulla stessa riga la URL di partenza e %{url_effective}: se le due colonne non coincidono il link va aperto a mano, anche con un 200 davanti. Google chiama soft 404 esattamente questo caso: 200 con dentro un errore.
Se poi il controllo lo automatizzi in PHP dentro il tema o un plugin, usa wp_safe_remote_head() e non wp_remote_head(). La differenza è reject_unsafe_urls, che passa la URL da wp_http_validate_url() e blocca IP interni e porte non standard. Le URL le ha scritte un modello a partire da un testo: sono input non fidato, e una richiesta server-side verso un indirizzo interno è una richiesta che parte dalla tua rete. Alza anche il timeout, perché il default di WP_Http è 5 secondi e un server lento diventa un falso positivo.
La take
La verifica dei link non è un controllo di qualità editoriale, è un controllo di integrità dei dati, come la validazione di un output JSON prima di scriverlo. Un link è un fatto binario: risolve o non risolve. Ogni volta che un fatto è binario e verificabile con un comando, chiedere a una persona di controllarlo a occhio è la scelta sbagliata: si dimentica sotto scadenza, proprio nei giorni pieni. Mettilo nel gate una volta e smetti di pensarci.
Fonti
- WordPress Developer Resources —
wp_safe_remote_head() - WordPress Developer Resources —
WP_Http::request(), argomenti e default - WP-CLI —
wp post list - Google Search Central — HTTP status codes e soft 404

