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