Higgins: come pubblico questo blog, e perché l’ho reso open source

Questo articolo — come tutti quelli recenti sul blog — non l’ho scritto da wp-admin.
L’ho scritto in un file Markdown, in locale, e un mio script lo ha caricato su
WordPress via SSH. Lo stesso script gli ha anche generato e impostato la copertina.
Si chiama Higgins, e da qualche giorno è su GitHub,
MIT.
Il problema
Wp-admin va benissimo per un sito che si aggiorna ogni tanto. Non va bene per un blog
che deve uscire tutti i giorni: l’editor a blocchi non si versiona, l’upload delle
immagini è manuale, e non c’è modo di dire “rifai questa pubblicazione esattamente
come l’altra volta” — ogni volta è a mano, ogni volta può andare storta in un modo
diverso.
La soluzione ovvia sarebbe la REST API di WordPress. L’ho scartata di proposito: vuol
dire un plugin da mantenere, application password da custodire, una superficie
d’attacco in più esposta pubblicamente. Quello che avevo già, e di cui mi fidavo, era
una chiave SSH verso il mio VPS. Ho costruito tutto su quella.
La soluzione: file Markdown, WP-CLI, niente altro
Un contenuto è un file:
---
title: Automazione dei processi aziendali
slug: automazione-dei-processi-aziendali
type: page
excerpt: Una riga di riepilogo, usata come meta description.
categories: [12]
---
## Titolo di sezione
Testo in Markdown normale.
Pubblicare significa una chiamata SSH che passa il contenuto già convertito in HTML
a blocchi Gutenberg allo stdin di wp post create (o wp post update, se lo slug
esiste già — idempotente, rilanciare non duplica):
python publish.py --dry-run contenuti/x.md # cosa farebbe, zero modifiche
python publish.py contenuti/x.md # online
Il --dry-run non è un optional: prima di ogni caricamento vero mostro sempre
l’output. Un contenuto già pubblicato si sostituisce per intero — non c’è una
copia di sicurezza automatica — quindi un file incompleto caricato per sbaglio
cancella quello che c’era prima. L’ho imparato riscrivendo una pagina di servizio con
un file di prova rimasto a metà: da quel giorno il dry-run è obbligatorio, non
raccomandato.
Le copertine, generate e mai a caso
Ogni articolo ha una copertina, generata da un secondo script (featured.py) che
prova in ordine Hugging Face, Cloudflare Workers AI e Gemini — il primo che risponde
vince, e con nessuno configurato lo dice chiaramente invece di improvvisare un
ripiego. Il prompt lo compongo da titolo, excerpt e tag, oppure lo scrivo a mano nel
front-matter con image_prompt:.
Cose che ho imparato a mie spese, generando qualche decina di copertine:
- I prompt corti funzionano meglio. Con un modello a 4 step di inferenza
(FLUX.1-schnell, quello che uso), un prompt di quattro righe produce clip-art
affollata; uno di due righe dà una composizione pulita. - Mai negazioni. Scrivere “no lightbulbs, no brains” nel prompt li rende più
probabili, non meno — il primo tentativo mi ha prodotto esattamente una testa con
un cervello disegnato dentro. Si descrive solo quello che si vuole vedere. - Una copertina già presente non si sovrascrive mai. Se un post ce l’ha, l’ha
scelta una persona: lo script lo verifica prima di generare qualsiasi cosa.
Il dettaglio che quasi mi è sfuggito: l’ordine delle operazioni
Un giorno ho collegato un plugin di auto-condivisione social al sito. Per farlo
funzionare bene serve una cosa non ovvia: se un post va online prima che la
copertina sia impostata, l’anteprima che Facebook e LinkedIn generano per quel link
resta senza immagine — e certi social se la tengono in cache, quindi non basta
aggiungerla dopo.
La correzione è stata cambiare l’ordine: un post nuovo nasce come bozza, prende la
copertina, e solo alla fine passa a pubblicato. Un contenuto già online, invece,
si aggiorna sul posto senza questo passaggio — rimetterlo in bozza per un istante lo
toglierebbe dal sito e farebbe scattare una seconda condivisione per un semplice
refuso corretto.
La coda che si autoalimenta
La parte che uso ogni giorno: un file Markdown fa da coda editoriale, con tre
sezioni — Coda, Pubblicati, Scartati. Un task pianificato lancia
Claude Code in modalità non interattiva una volta
al giorno: prende la prima voce, scrive il post, lo pubblica, sposta la voce in
Pubblicati. Se la coda è vuota, prima se la rigenera da sola, controllando via
wp post list di non riproporre niente di già scritto.
La regola che tiene tutto onesto è una sola: se un argomento richiederebbe un dato
che lo script non ha — un numero, un dettaglio tecnico, un fatto verificabile — quella
voce finisce in Scartati con il motivo, e si passa alla successiva. Non si inventa
mai per fare numero. Vale anche per gli argomenti di sicurezza: un post su una CVE
esce solo se ogni dato è verificato su una fonte primaria durante la scrittura, mai
a memoria — altrimenti si salta.
Cosa evitare, se lo replichi
- Non affidarti alla REST API se non ti serve davvero. SSH più WP-CLI, con
l’utente limitato via sudoers a un solo comando, è una superficie d’attacco molto
più piccola. --dry-runsempre, prima di ogni pubblicazione vera. È il controllo più
economico che esista contro un front-matter sbagliato.- L’encoding dello stdin, su Windows, non è scontato.
subprocess.run(...,senza specificare
text=True)encoding="utf-8"codifica in cp1252: qualunque
contenuto con caratteri accentati arriva corrotto e WordPress lo rifiuta con un
errore poco parlante. Va dichiarato esplicitamente. - Un autore mancante rompe più cose di quanto sembri. Un post creato da riga di
comando senza--post_authorfinisce con autore 0, cioè nessuno: si vede sul
sito, e se hai un plugin di auto-condivisione social le sue connessioni sono per
utente WordPress — senza autore, quel post non va da nessuna parte.
Il codice è tutto su github.com/simonecosci/higgins,
con un README che spiega come configurarlo da zero, chiave SSH inclusa. Se lo provi
e trovi un buco, sono curioso di saperlo.
