Torna a tutti gli articoli

Da WordPress sul Raspberry Pi a Cloudflare Workers: la migrazione passo per passo

7 min di lettura Automation, Cloudflare, Raspberry Pi

WordPress to Astro and Cloudflare Workers

Fin dall’inizio questo blog è stato un sito WordPress servito dal Raspberry Pi 5 di casa, attraverso un Cloudflare Tunnel. A ottobre Google Search Console ha segnalato errori del server (5xx). La causa non era WordPress, era la mia linea internet: il 30 ottobre il tunnel è rimasto scollegato per circa un’ora, l’1 novembre per quasi quattro ore. In quelle ore ogni visitatore, e anche Googlebot, non trovava il server.

Volevo tenere WordPress, perché mi piace scriverci e Rank Math gestisce la SEO. Così ho separato i due compiti. WordPress resta sul Pi, ma solo come gestionale. Il sito pubblico ora è un insieme di pagine statiche su Cloudflare. Quando il Pi o la linea si fermano, chi legge non vede differenze.

In questo articolo trovi cosa ho fatto, passo per passo: GitHub, Workers Builds, Cloudflare Access, lo spostamento del dominio e le impostazioni di WordPress. Trovi anche i tre problemi che mi hanno fermato, perché ognuno può fermare anche te.

Prima: i visitatori arrivano a WordPress sul Raspberry Pi attraverso un Cloudflare Tunnel. Dopo: i visitatori arrivano a pagine statiche su un Cloudflare Worker, e WordPress sta su cms.raffaello.uk dietro Cloudflare Access.
Prima e dopo. Il Pi c’è ancora, ma chi legge non dipende più da lui.

I pezzi

  • WordPress sul Pi, ora su cms.raffaello.uk. Cloudflare Access protegge tutto l’indirizzo.
  • Astro costruisce il sito pubblico. Legge articoli, pagine, categorie e i tag SEO di Rank Math dalle API REST di WordPress. Copia ogni immagine nella build.
  • Un repository privato su GitHub contiene il codice Astro.
  • Cloudflare Workers Builds costruisce il sito da GitHub e lo pubblica.
  • Cloudflare Workers static assets serve le pagine su raffaello.uk. Non c’è nessuno script, solo file.

Passo 1: metti il sito Astro su GitHub

Il progetto Astro mantiene gli URL di WordPress: /slug/ per l’inglese, /it/slug/ per l’italiano, e gli archivi delle categorie. Il file wrangler.jsonc dice a Cloudflare di servire la cartella dist come file statici:

{
  "name": "raffaello-blog",
  "compatibility_date": "2026-11-01",
  "assets": {
    "directory": "./dist",
    "html_handling": "auto-trailing-slash",
    "not_found_handling": "404-page"
  }
}
  1. Crea un repository privato su GitHub.
  2. Carica il progetto Astro sul branch main.
  3. Non mettere segreti nel repository. La build li prende da Cloudflare (passo 4).

Passo 2: collega il repository a Workers Builds

  1. Nella dashboard di Cloudflare, apri Workers & Pages.
  2. Crea un Worker e scegli Import a repository.
  3. Dai a Cloudflare l’accesso al repository GitHub.
  4. Imposta il comando di build: npm run build.
  5. Imposta il comando di deploy: npx wrangler deploy.
  6. Imposta il branch di produzione: main.

Da qui, ogni push su main fa partire una build, e una build riuscita va online. Gli altri branch creano versioni di anteprima su un indirizzo workers.dev. Io ne uso una per la nuova grafica di questo blog.

Nota: le anteprime hanno variabili di build proprie. Se le imposti solo per la produzione, le build di anteprima non entrano in WordPress.

Passo 3: metti WordPress dietro Cloudflare Access

WordPress non deve più essere pubblico, ma la build gira sulle macchine di Cloudflare, non sulle mie. Le serve una chiave sua. In Access questa chiave si chiama service token.

  1. Nel tunnel, aggiungi una route pubblicata: cms.raffaello.uk verso il container di WordPress.
  2. Apri Zero Trust → Access controls → Service credentials.
  3. Crea un service token. Copia il Client ID e il Client Secret. Cloudflare mostra il secret una volta sola.
  4. Apri Policies. Crea una policy con azione Service Auth che include il service token.
  5. Crea una seconda policy con azione Allow che include il tuo indirizzo email.
  6. Apri Applications. Crea un’applicazione self-hosted per cms.raffaello.uk.
  7. Collega tutte e due le policy all’applicazione.

Attenzione: il passo 7 è quello che avevo saltato. La mia policy esisteva, ma la colonna Used by applications diceva 0. Ogni build si fermava con un 302, cioè un rimando alla pagina di login di Access. Se vedi un 302 da Access, guarda prima quella colonna.

Passo 4: dai il token alla build

  1. Apri il Worker, poi Settings → Build → Variables and secrets.
  2. Aggiungi WP_CMS con il valore https://cms.raffaello.uk.
  3. Aggiungi CF_ACCESS_CLIENT_ID con il Client ID.
  4. Aggiungi CF_ACCESS_CLIENT_SECRET come secret, con il Client Secret.

La build manda i due valori come header in ogni richiesta a WordPress. In più rifiuta i redirect: così un token sbagliato ferma la build con un errore chiaro, invece di costruire un sito vuoto:

const headers = {
  'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID,
  'CF-Access-Client-Secret': process.env.CF_ACCESS_CLIENT_SECRET,
};
const res = await fetch(url, { headers, redirect: 'manual' });
if (!res.ok) throw new Error(`WordPress ha risposto ${res.status}: controlla il service token di Access`);

Per trovare la causa del mio 302, ho fatto scrivere nel log della build se le credenziali c’erano, e quanto erano lunghe. Il log non scrive mai i valori.

Passo 5: guarda cosa pubblica la build

Sistemato Access, la build era verde e il deploy falliva: Asset too large. Workers static assets accetta file fino a 25 MiB. Il mio script copiava tutta la libreria media, e nella libreria c’era wxr.xml: un export di 36 MB dell’intero blog, che avevo caricato l’anno scorso. Era troppo grande, e non deve essere pubblico.

  1. Copia dalla libreria media solo le immagini (/wp/v2/media?media_type=image).
  2. Salta ogni file sopra i 25 MiB, con un avviso nel log.
  3. Togli export e backup dalla libreria media di WordPress. Tienili in un posto privato.

Passo 6: fai un test che blocca un deploy sbagliato

Prima di spostare il dominio, ho messo in un file tutti gli URL che Google conosceva dalle vecchie sitemap di WordPress. Poi li ho confrontati con il sito nuovo. I 22 tra articoli e pagine c’erano. Mancavano tutti e 19 gli archivi delle categorie. Girando per il sito con il mouse non me n’ero accorto.

Così quella lista è diventata un test. Il comando di build ora è:

node scripts/fetch-media.mjs && astro build && node scripts/check-build.mjs

check-build fa tre controlli. Se uno fallisce, la build si ferma e Cloudflare non pubblica niente:

  1. Ogni URL delle vecchie sitemap esiste nel sito nuovo (37 URL).
  2. Nessuna pagina punta a cms.raffaello.uk, perché chi legge non può aprirlo.
  3. Nessuna pagina contiene un modulo di WordPress, perché una pagina statica non può inviarlo.

Il controllo 3 ha trovato un problema vero. La pagina contatti aveva un modulo SureForms che inviava i dati ad admin-ajax.php. Sul sito statico sembrava a posto e non inviava niente. Ora la pagina mostra un indirizzo email, che Cloudflare Email Routing inoltra alla mia casella.

Passo 7: tieni l’indirizzo di prova fuori da Google

Il sito risponde anche su un indirizzo workers.dev. Google deve indicizzare solo raffaello.uk. Metti questo in public/_headers:

https://raffaello-blog.<tuo-sottodominio>.workers.dev/*
  X-Robots-Tag: noindex

I vecchi indirizzi di WordPress vanno al loro posto nuovo con public/_redirects: /wp-admin/* e /wp-login.php verso il CMS, /feed/ verso /rss.xml, /sitemap_index.xml verso /sitemap-index.xml.

Passo 8: sposta il dominio

Lo stesso tunnel serve molti altri servizi del mio Pi. Prima di toccarlo ho registrato la risposta HTTP di ogni indirizzo. Dopo la modifica l’ho confrontata di nuovo.

  1. Nel Worker, apri Domains e aggiungi www.raffaello.uk. Non aveva record DNS, quindi nessun conflitto.
  2. Prova ad aggiungere raffaello.uk. Cloudflare rifiuta, perché il record del tunnel esiste ancora.
  3. In Networking → Tunnels, apri il tunnel e poi Routes. Elimina la route di raffaello.uk. Aspetta il messaggio Route deleted successfully. Elimina anche il record DNS.
  4. Aggiungi subito raffaello.uk al Worker.
  5. In Rules → Redirect Rules, usa il modello Redirect from WWW to root. Manda un 301 da www a raffaello.uk. Seleziona Preserve query string.

Il sito è rimasto offline per circa un minuto. Gli altri indirizzi, i record della posta, DKIM e DMARC non sono cambiati.

Attenzione: al punto 3, aspetta la conferma prima di cambiare pagina. La prima volta ho cambiato pagina troppo presto e l’eliminazione non è avvenuta.

Passo 9: trasforma WordPress in un gestionale

Nel container di WordPress, l’indirizzo del sito e l’indirizzo di WordPress ora sono diversi:

define('WP_HOME', 'https://raffaello.uk');         // link e canonical non cambiano
define('WP_SITEURL', 'https://cms.raffaello.uk');  // admin, API REST, upload
define('DISABLE_WP_CRON', true);
  • API REST. L’editor a blocchi costruisce l’indirizzo REST da WP_HOME, e raffaello.uk/wp-json non è più WordPress. Un piccolo plugin “must-use” cambia l’indirizzo verso il CMS con il filtro rest_url.
  • Cron. WordPress non può chiamare sé stesso passando da Access. Gli articoli programmati hanno bisogno del cron, quindi è il Pi a chiamarlo ogni minuto sulla porta locale:
    * * * * * curl -s -o /dev/null http://127.0.0.1:8888/wp-cron.php
  • Immagini. I nuovi upload ora hanno indirizzi cms.raffaello.uk. La build li cambia in raffaello.uk e copia i file, e il controllo 2 lo verifica.

Passo 10: ricostruisci il sito quando pubblico

  1. Nel Worker, apri Settings → Build → Deploy Hooks.
  2. Crea un hook per il branch main. Tieni l’URL segreto: fa partire una build.
  3. Salva l’URL in WordPress, non nel codice.

Lo stesso plugin “must-use” chiama l’hook quando pubblico, aggiorno o ritiro un articolo o una pagina. L’editor a blocchi salva più di una volta, quindi il plugin aspetta un minuto e poi fa partire una build sola.

Sei passaggi: pubblica in WordPress, deploy hook, scarica articoli e immagini, build Astro, check-build, deploy su Workers. Se check-build fallisce, non si pubblica niente.
Dal pulsante Pubblica al sito online.

Passo 11: avvisa Google

  1. In Search Console, apri Sitemaps. Invia https://raffaello.uk/sitemap-index.xml.
  2. Apri Pages → Server error (5xx). Seleziona Validate fix.

Cosa ho imparato

  • Se Access rimanda alla pagina di login, controlla che la policy sia collegata all’applicazione, non solo che esista.
  • Una lista dei vecchi URL usata come test trova più cose di un lungo giro sul sito. La mia ha trovato le categorie e il modulo.
  • Guarda cosa pubblica la tua build. La mia stava per mettere online un export completo del blog.
  • Prima di cambiare un tunnel condiviso, registra lo stato di ogni indirizzo. Poi confronta.

Ho fatto questa migrazione con Claude Code, sul Pi e nel browser. Ha trovato e risolto i problemi di questo articolo, e io ho preso le decisioni. Il Pi fa ancora girare WordPress, i bot di trading e tutto il resto. Solo che ora non deve più tenere online il blog.