+39 3662317539/+34 620899163
info@simonecosci.com

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

Sviluppo Siti Web a Tenerife

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-run sempre, 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(...,
    text=True)
    senza specificare 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_author finisce 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.