Vai al contenuto

Una testata diaspora migrata e ottimizzata in 18 giorni

Una testata diaspora migrata e ottimizzata in 18 giorni

Una testata diaspora migrata e ottimizzata in 18 giorni

Luglio 2025, audit di stack su una testata della diaspora rumena in Italia. Lo storico del traffico saliva da mesi, il backend WordPress mostrava query MySQL p99 sopra 4 secondi durante i picchi serali e il TTFB medio in PageSpeed era oltre 1,8s. La migrazione testata diaspora rumena è partita da lì, da una scrivania di lavoro condivisa in screen share con il referente tecnico del cliente. Diciotto giorni dopo il sito girava su una piattaforma multi-tenant con redirect 301 di tutto lo storico, infra dedicata e tuning di performance verificato in produzione. Vedi anche gli altri post di questa rubrica: Fieldwork.

Il cliente, anonimizzato

Un quotidiano in lingua diversa dall’italiano per una comunità di lettori in Italia, audience verticale, redazione piccola con cadenza giornaliera. Stack legacy: WordPress su shared hosting di fascia bassa, plugin di traduzione installato anni prima e mai aggiornato a multi-lingua nativo, tema custom forkato due volte, nessuna CDN davanti al backend. Traffico in crescita perché il bacino di lettori si era allargato e i social inviavano picchi non gestibili. Il nome non lo scrivo: in Fieldwork il cliente lo cito solo con consenso esplicito scritto. Qui parlo del lavoro.

Audit: i tre punti che decidono il piano

Prima di toccare codice, due giorni di audit con il referente tecnico, screen share aperto, log e metriche del backend di produzione sotto gli occhi. Tre punti emersi:

  1. Database non sano. Tabelle wp_postmeta con righe orfane oltre il 30% del totale, indici mancanti su meta_key per le query del tema, autoload caricato a ogni request con valori obsoleti. La query SELECT * FROM wp_options WHERE autoload='yes' restituiva quasi 4 MB di dati per request.
  2. Nessuna CDN, nessun layer di cache HTTP. Origin direttamente esposto, immagini servite senza dimensioni esplicite e senza variant responsive, font caricati da terze parti senza preconnect.
  3. Plugin di traduzione legacy. Mantenere il bilinguismo richiedeva di estrarre i contenuti come tabelle sorelle, non come custom post types puliti.

L’output dell’audit non era una lista di task, era un piano di migrazione con tre milestone numerate e una finestra di rollback per ogni step.

Architettura target

Multi-tenant editoriale: il sito è entrato come nuovo tenant sopra la piattaforma AI Multisite di Romiltec, con WordPress come CMS frontale e il backend Laravel che gestisce orchestrazione, sync e (in seconda fase) automazione editoriale. Stack lato sito: WordPress in container Docker (PHP 8.3, OPcache, Redis object cache); tema ricostruito su block-editor compatibile, tipografia di sistema, immagini WebP/AVIF con fallback e srcset completo; Nginx davanti al container con micro-cache 60s per archivi e bypass su cookie loggato; Cloudflare in Full Strict TLS con regole su feed e multilingua; MariaDB dedicata, innodb_buffer_pool_size tarato e query_cache_type=0 (il legacy lo aveva ancora attivo, con risultati pessimi).

Il bilinguismo è gestito come due tenant logici sulla stessa codebase, con tenant_id che propaga attraverso queue, cache key e log. Schema WordPress unico, non network multisite: con cinque persone il costo operativo di N database è insostenibile, e lo schema condiviso è risultato più semplice da osservare e da migrare.

La migrazione contenuti

Pipeline in tre stadi. Niente WXR (export nativo XML di WordPress) come unica fonte: il plugin di traduzione legacy segmentava male e perdevamo gli abbinamenti italiano/rumeno.

Stadio 1, dump strutturato. Script PHP CLI sul backend legacy che leggeva il database e produceva un JSON normalizzato per articolo: id legacy, slug, lingua, title, body, autore, categorie, tag, featured image url, embed. Output gzippato, sha256 per integrità.

Stadio 2, normalizzazione. Script Python: rewrite degli URL interni nei body, download delle immagini con retry (lo shared hosting cadeva spesso), normalizzazione dei caratteri (alcuni post avevano caratteri misti UTF-8 e cp1250 dallo stesso editor su browser diverso), generazione varianti WebP/AVIF per le featured.

Stadio 3, import idempotente. Endpoint POST sul nuovo backend che creava o aggiornava il post target via REST API WordPress (Application Passwords, Bearer per il backend Laravel). Idempotenza tramite chiave composita (legacy_id, lingua): rilanciare lo script non duplicava nulla, aggiornava i delta. Timestamp originale preservato. Ogni post atterrava in draft finché un secondo script non lo promuoveva in batch, ordine deterministico per data.

Volumi: decine di migliaia di articoli storici, sette anni di contenuti, decine di GB di media. La pipeline è girata in tre cicli: validazione mapping, bulk, delta del blocco redazione.

Redirect 301: il SEO non si rompe per pigrizia

La parte più sensibile per un cliente editoriale. URL legacy del tipo /2018/03/12/post-name/ dovevano puntare ai nuovi /post-name/ (permalink ridotti, decisione presa con il referente per coerenza con altri siti del bacino). Per ogni post, lo script di import scriveva una riga in una tabella redirect_map sul nuovo backend.

A cutover il file della mappa è stato compilato in regole Nginx native (no plugin redirect, troppo costoso a runtime con 18.000 pattern). Generazione automatica via Jinja2: input la tabella, output /etc/nginx/conf.d/redirects-tenant.conf, reload del processo senza downtime via nginx -s reload. Tre giorni dopo il cutover ho fatto un crawl completo con wget --spider partendo da una sitemap legacy salvata da archive.org più la sitemap del backend storico: zero 404 su URL pubblicati, una manciata di 404 su attachment cancellati anni prima (li abbiamo lasciati 410 Gone, scelta esplicita).

Performance: dalla baseline ai numeri post-migrazione

Misurazioni prese con il referente tecnico in screen share, due strumenti: Google PageSpeed Insights (Lighthouse field data lato mobile) e WebPageTest da nodo Milano, tre run, mediana.

Baseline (giorno zero, sito legacy):
– TTFB mobile p75: 1,84 s
– LCP mobile p75: 4,2 s
– CLS p75: 0,21
– Total page weight homepage: 5,8 MB

Target post-migrazione (giorno 18, infra nuova, stesso URL canonico):
– TTFB mobile p75: 0,42 s
– LCP mobile p75: 1,9 s
– CLS p75: 0,02
– Total page weight homepage: 1,3 MB

I numeri sono questi perché il TTFB lo decide lo stack, non il tema: passare da shared hosting con MySQL al 90% di CPU a un container Docker dietro Nginx micro-cache dimezza il tempo prima di scrivere il primo byte. Il resto è igiene: immagini con dimensioni esplicite, niente font esterni, JS dei consensi caricato in defer.

Workflow post-migrazione e cosa è andato storto

Quindici giorni di lavoro tecnico più tre di affiancamento alla redazione. Sopra il backend WordPress, AI Multisite come hub per pubblicazione assistita (suggerimenti su titoli e meta, non riscrittura), traduzione preservando lo stile (DeepL primario, fallback LLM con istruzioni stilometriche per i pezzi di opinione), check di duplicazione interna prima della pubblicazione. L’AI propone, l’editor decide: regola architetturale del prodotto.

Due intoppi reali. In staging sul campione di mille post zero problemi; in produzione il ciclo si è bloccato a 12.400 perché il lock di scrittura di wp_insert_post collideva con il cron Typesense che indicizzava ogni nuovo post. Disattivato il cron durante l’import bulk, riattivato dopo, indicizzazione full ricostruita dal worker Laravel in background. Secondo: alcune immagini storiche avevano EXIF corrotto (upload via FTP da macchine fotografiche di dieci anni fa), la pipeline AVIF falliva silenziosamente. Fix: try/except esplicito, fallback su JPEG ottimizzato con jpegoptim, log per audit manuale (230 immagini in totale, tutte storiche, nessuna sui post degli ultimi due anni).

I numeri di chiusura

Metrica Valore
Giorni di lavoro effettivo 18
Articoli migrati decine di migliaia
Media migrati decine di GB
Redirect 301 generati oltre 18.000
Riduzione TTFB p75 da 1,84s a 0,42s
Riduzione LCP p75 da 4,2s a 1,9s
Riduzione page weight homepage da 5,8 MB a 1,3 MB
Persone Romiltec coinvolte 2 (un senior dev, io sul DevOps e sul piano)

Quello che mi sono portato a casa

Prima. Un audit di due giorni vale quanto due settimane di sviluppo cieco. Le tre cose che servono al sito le vedi solo se apri lo screen share e guardi le metriche reali, non se leggi il brief.

Seconda. I redirect 301 si fanno bene una volta, in compilazione, non a runtime. 18.000 pattern in un plugin redirect costano millisecondi a request: gli stessi 18.000 in nginx.conf con ngx_http_rewrite_module costano zero (lookup hash). È la differenza fra una migrazione che lascia il sito veloce e una che lo lascia lento per pigrizia.

Terza. Una migrazione editoriale non è un singolo deploy, è una pipeline idempotente che può girare tre volte senza romperti niente. Il primo round serve a validare, il secondo a portare il bulk, il terzo a recuperare i delta del blocco redazione. Se il tuo script non è idempotente, non hai una migrazione: hai una scommessa.

Diciotto giorni, due persone, un cliente che non si accorge del cutover (era previsto un blocco di redazione di 90 minuti, è durato 38). Il pezzo che ho imparato da questo lavoro non è tecnico: è che le metriche pre/post sono il vero output di una migrazione. Se non le hai prese al giorno zero, non sai cosa hai consegnato.