Tinexta InfoCert - design system e page builder proprietario
Design system enterprise e page builder proprietario per il nuovo sito di Tinexta InfoCert: 46 pattern componibili su tema WordPress, Tailwind v4 e fedeltà pixel-perfect al Figma.

Il design system enterprise e il page builder proprietario alla base del nuovo sito commerciale di Tinexta InfoCert, il gruppo che in Italia si occupa di firma digitale, PEC, conservazione, identità digitale e fatturazione elettronica. Il progetto sostituisce le installazioni ancora online su infocert.it e infocert.digital con un tema WordPress custom in cui la libertà di composizione editoriale convive con la fedeltà pixel-perfect al design system Figma, verificata da un audit esterno.
Ho lavorato come lead developer con responsabilità di product owner tecnico, coordinando quattro sviluppatori in fase di build e curando il passaggio di consegne al team interno del cliente. Ho definito le fondazioni del tema, scritto i componenti a più alta complessità e portato il design system dai token alla verifica in browser.
| Aspetto | Dettaglio |
|---|---|
| Ruolo | Lead developer e product owner tecnico: fondazioni, architettura, componenti verticali, QA e handover |
| Periodo | Gennaio-luglio 2026, 24 settimane attive di sviluppo |
| Team | 4 sviluppatori in build, più il team interno del cliente in consolidamento |
| Stack | WordPress, PHP 8, ACF Extended, Tailwind CSS v4, JavaScript vanilla (ES Modules), Swiper 11 |
| Tooling | Figma, Jira, Bitbucket, esbuild per la minificazione |
| Codice | 36.066 righe (23.035 PHP, 6.808 CSS, 6.210 JS), zero framework front-end |
| Vincolo | Fedeltà pixel-perfect al design system su 3 breakpoint, verificata da audit esterno |
Il problema: massima libertà di composizione e massimo controllo del rendering
InfoCert doveva ricostruire il proprio sito commerciale con tre vincoli che tiravano in direzioni opposte. Il primo era l'autonomia editoriale totale, con il marketing in grado di comporre qualunque pagina come mattoncini, senza passare dallo sviluppo. Il secondo era la fedeltà al design system Figma fino al singolo pixel di gap, su tre breakpoint e per decine di componenti, con un audit esterno a certificarla. Il terzo era l'assenza di framework front-end, per lasciare al team interno un tema WordPress classico e manutenibile.
Un page builder generico come Gutenberg o Elementor avrebbe risolto il primo vincolo distruggendo il secondo, e un set di template fissi avrebbe fatto l'opposto. La risposta è stata un design system codificato in un page builder proprietario ACF-driven: un catalogo chiuso di 46 pattern progettati sul design, componibili liberamente, ognuno con le sue varianti dichiarate. L'editor ha libertà combinatoria, il rendering resta sotto controllo dello sviluppo.
La decisione di prodotto più impattante non è stata scrivere codice, ma definire il catalogo dei pattern: quali componenti esistono, quali varianti espone ciascuno, cosa è configurabile dall'editor e cosa no. È ciò che il marketing potrà fare per anni senza chiamare uno sviluppatore.
L'architettura: quattro strati e una sola regola portante
Il tema è organizzato in quattro strati con dipendenze a senso unico: i template chiamano un dispatcher unico, il dispatcher risolve i pattern, i pattern compongono i componenti riusabili, i componenti si appoggiano ai moduli di servizio. La regola strutturale portante è una sola, cioè che solo i pattern conoscono ACF. I 25 componenti riusabili non chiamano mai get_sub_field, ricevono un array già normalizzato.
È una regola sola, ma è quella che rende i componenti utilizzabili anche fuori dal page builder, negli archivi, nei template articolo, nelle colonne della tabella comparativa, senza doverli duplicare.
La convenzione di naming come sistema di build
La decisione tecnica di cui vado più orgoglioso è anche la più semplice. Il nome di un layout ACF risolve, per pura convenzione, quattro artefatti diversi: il pattern PHP, il suo CSS, il suo JavaScript e la thumbnail di anteprima nel back office.
Da questa convenzione discende tutto il resto senza scrivere altro codice. Un solo dispatcher serve l'intero sito: flexible-loop.php sono 51 righe, senza registry, senza mappa, senza switch. Il caricamento degli asset è condizionale e gratuito, perché all'enqueue leggo i layout presenti nella pagina e carico solo i CSS e i JS corrispondenti. E non esiste un punto di rottura silenzioso, dato che rinominare un layout in ACF senza rinominare i file lo fa smettere di funzionare subito e in modo visibile.
- 51 layout dichiarati, 46 file, zero orfani e zero eccezioni hardcoded a progetto concluso
- Lo stesso dispatcher serve 4 campi flexible distinti e 2 template completamente diversi
- Aggiungere un pattern significa aggiungere file, non modificare registri o switch
Quando a giugno è servito il template Articolo a tre colonne (hero, colonna indice, corpo, colonna prodotto) non ho scritto un secondo motore di rendering: ho parametrizzato quello esistente su quattro assi e riusato gli stessi 46 pattern nella colonna centrale.
Componenti a contratto, non a implementazione
Ogni componente riusabile espone un contratto documentato in un blocco @example che mostra la chiamata completa. In un progetto PHP senza tipi, quel blocco è la type definition, ed è ciò che ha permesso a quattro sviluppatori di usare gli stessi componenti senza leggerne il codice. L'adozione si misura nei numeri: il componente bottone è invocato da 51 punti del tema, nessun pattern scrive un bottone a mano.
Si misura anche al contrario, nella coda corta dei pattern. block-accordion.php sono 29 righe, block-boxedSection.php 26, block-customCode.php 22: pattern che si limitano ad adattare i campi ACF a un componente esistente. Quanto sono corti i pattern è la misura di quanto lavoro è stato spinto giù nei componenti.
Componenti disaccoppiati per contratto DOM
Per i componenti con comportamento ho scelto un disaccoppiamento più forte del semplice import: il contratto è il DOM, non il modulo JavaScript. Lo scrollbar custom finito in 14 pattern non conosce nessuno dei suoi consumatori. Espone tre interfacce: custom property CSS per lo styling, attributi data-* per il comportamento (mai classi, così si può rifattorizzare il CSS senza rompere il JS) ed eventi custom in bubbling per l'integrazione. Sono i pattern ad ascoltare l'evento e a pilotare Swiper, non il contrario, e non esiste un solo import incrociato tra i moduli JS del tema.
Il design system in Tailwind v4
Tailwind v4 è stato scelto per la configurazione CSS-first. Non esiste un tailwind.config.js nel progetto: il design system vive interamente in un file CSS, leggibile da chiunque conosca il CSS, incluso il team che avrebbe ereditato il tema. Sono 44 token @theme, di cui 30 colori, con nomi che seguono il dominio di business e non l'estetica.
Ad aprile ho fatto un refactor che ha eliminato le varianti generiche light e dark sostituendole con i colori di settore (--color-sector-pmi, --color-sector-corporate, --color-sector-pa, i tre segmenti commerciali di InfoCert). Un cambio che ha reso impossibile scegliere il blu al posto del colore del segmento PA, spostando una decisione di brand dal template al token.
Le 25 utility tipografiche sostituiscono le utility text-* di Tailwind. Ognuna incapsula peso, dimensione, interlinea, spaziatura e famiglia, e dove serve porta al suo interno il proprio breakpoint desktop.
Il markup scrive class="h1" e ottiene il comportamento responsive corretto ovunque: la scala tipografica ha un solo punto di verità e un designer può verificarla leggendo un file.
Cinque nomi di breakpoint su tre soglie reali
È la decisione più contro-intuitiva del design system. Il design ha tre soglie (mobile, tablet da 768, desktop da 1280), ma un dev che scrive md: per abitudine finirebbe fuori griglia. Facendo collassare sm e md su 768 e xl e 2xl su 1440, qualunque prefisso si scriva il risultato resta dentro il design system.
Ho scambiato la precisione semantica dei nomi con l'impossibilità di sbagliare. Su un team di cinque persone, sei mesi e centinaia di componenti, è stato lo scambio giusto. La disciplina mobile-first si legge nei numeri: 61 media query min-width contro 8 max-width usate come override puntuali.
Due componenti in profondità
Product card: 1.341 righe, il cuore commerciale del sito
L'ho creata ed è rimasto il componente che vende: compare in 19 punti del tema, dentro 13 pattern diversi, dalla hero alla tabella comparativa al rail dell'articolo. La stessa card deve funzionare come tile di un carosello, colonna di una comparativa, blocco promozionale, riquadro prezzo dentro una hero e solo-prezzo dentro un box informativo.
Le alternative erano tre: cinque componenti separati (duplicazione e divergenza garantita nel tempo), un componente con cinque rami condizionali (rigidità), oppure un componente parametrizzato su assi ortogonali. Ho scelto la terza, con tre livelli distinti di parametrizzazione.
| Livello | Meccanismo |
|---|---|
| Presenza delle sezioni | 6 flag booleani have_* che accendono o spengono intere parti (immagine, header, descrizioni, inclusioni, cross-selling, scadenza) |
| Modalità di rendering | price_only come uscita anticipata (solo prezzo e CTA), più pattern_type e price_layout con 4 valori ciascuno |
| Override di layout | classi iniettabili dal chiamante, dichiarate una per una con il motivo d'uso |
Il terzo livello è quello che di solito degenera in un buco nero di class arbitrarie. L'ho tenuto stretto: ogni override è un parametro nominato, con un commento che dice chi lo usa e perché.
Il motore prezzi
La card non riceve un prezzo, riceve un codice prodotto e lo risolve su un catalogo JSON centralizzato, versionato in git e caricato con cache singleton. Da lì deriva tutto: il parsing degli importi è agnostico rispetto al locale (riconosce 1.234,56 e 1,234.56 distinguendo il separatore decimale da quello delle migliaia in base alla posizione), l'IVA al 22% è calcolata server-side, il prezzo mensile è derivato dall'annuale e la percentuale di sconto è calcolata, mai digitata.
Il prezzo precedente compare solo se effettivamente maggiore di quello corrente, come validazione implicita contro l'errore redazionale, e il disclaimer di rinnovo si attiva da solo quando esiste un prezzo di listino. Sono regole di business che vivono nel software e non nel processo editoriale: un errore di prezzo su un sito commerciale è un problema legale, e qui l'editor non ha modo di introdurne uno digitando male una cifra.
Coerenza server e client senza chiamate di rete
Il selettore di varianti cambia prezzo, IVA, sconto e call-to-action senza alcuna richiesta di rete: ogni opzione porta con sé 21 attributi data-* e il JavaScript ricostruisce il rendering leggendoli. Perché il risultato sia identico a quello del server, le funzioni di parsing e formattazione prezzo sono portate 1:1 da PHP a JavaScript.
PHP risolve il codice prodotto sul catalogo, calcola IVA, sconto e prezzo mensile e stampa il markup con 21 attributi data-* per ogni variante. È la sorgente di verità del prezzo.
Accessibilità del selettore
Il selettore varianti è una combobox ARIA 1.2 completa: role=listbox, aria-selected, navigazione con le frecce, Home ed End, Escape che chiude e restituisce il focus al trigger, validazione annunciata via role=alert con aria-live. Sotto c'è un select nativo che fa da data store, rimosso dall'accessibility tree per non esporre due volte lo stesso controllo. Gli ID sono generati con wp_unique_id, perché la stessa card può comparire più volte nella stessa pagina.
Comparison table: una matrice, sei layout
Questo componente è stato sviluppato dal team; l'ho seguito come reviewer e integratore, e lo cito perché è il banco di prova più duro dell'architettura: 886 righe di pattern, 1.137 di CSS, 281 di JavaScript. La stessa matrice di dati deve diventare sei layout diversi, cioè tre tipologie (feature interne, feature laterali, accordion) per due modalità (griglia su desktop, tab con pannelli su mobile e tablet).
La soluzione strutturale è la separazione netta in due fasi: 380 righe di normalizzazione senza un solo carattere di output, poi 500 righe di rendering che leggono soltanto il modello normalizzato. È lo stesso principio di un controller che prepara un view-model, e il beneficio si vede quando serve il settimo layout, perché si tocca solo la seconda metà del file.
Due dettagli esemplari. La conta delle colonne viaggia come custom property CSS invece che come classe, così il CSS calcola repeat(var(--comparison-columns), minmax(0, 1fr)) senza dover generare in safelist grid-cols-2, -3 e -4 per ogni variante. E la semantica tabellare è ricostruita a mano: dove il layout è troppo divergente per usare un table, la struttura per gli screen reader è restituita con role=table, role=row, role=columnheader e role=cell.
Performance: i costi strutturali del page builder
A maggio ho condotto una campagna di ottimizzazione mirata, cinque interventi in una giornata, ciascuno con la motivazione e il beneficio misurato scritti nel messaggio di commit. Non erano micro-ottimizzazioni: erano i costi strutturali di un page builder con 46 pattern, che tende a caricare gli asset di tutti i pattern su tutte le pagine.
Asset condizionali sui layout attivi
Rimossi i 23 import statici dei block-*.js e sostituiti con un loop sui layout presenti in pagina, che fa wp_enqueue_script_module solo dei file esistenti. Una pagina con 3 layout carica 3 file JS invece di 30.
Swiper self-hostato e caricato su richiesta
Swiper è stato tolto dalla CDN esterna, self-hostato e caricato solo se in pagina esiste almeno uno dei 13 layout che lo usano. Circa 173 KB risparmiati su ogni pagina senza slider, più un DNS lookup esterno eliminato.
Font variabile self-hostato
Albert Sans in formato variabile: 4 file @font-face invece di sedici statici, con unicode-range separato e font-display: swap. Le quattro richieste a Google Fonts sono state unificate ed eliminate.
Critical CSS e lazy loading
Preload dei CSS critici con ordine di cascata dichiarato, dequeue di dashicons e dei CSS del block editor mai usati, lazy loading automatico delle immagini below-the-fold con opt-out per l'hero, che resta eager perché è l'LCP.
Minificazione con esbuild
A giugno ho aggiunto una pipeline con esbuild che serve i file .min in produzione e i sorgenti in sviluppo, con un risparmio misurato attorno al 60% sui file del tema.
Un elemento mantenuto di proposito: due sole dipendenze npm (Tailwind e la sua CLI), nessun bundler, nessun framework JavaScript, moduli ES nativi. Per un tema destinato alla manutenzione di un team interno, un npm install che non si può rompere vale più di qualunque ottimizzazione di build.
Accessibilità come infrastruttura
L'accessibilità non è stata una fase finale ma un vincolo di componente. I numeri sull'intero tema: 501 attributi ARIA, 163 usi di focus-visible (e nessun outline: none senza sostituto), 63 role, focus trap completo sul megamenu, attributo inert applicato agli slide fuori viewport.
Un plugin Swiper globale di dieci righe
Gli slide fuori dal viewport restano raggiungibili da tastiera e screen reader, il difetto classico di ogni carosello. Invece di correggerlo in tredici pattern, è registrato una volta su Swiper.use() e sincronizza l'attributo inert su tutti gli slider del sito.
Un modulo dedicato ai link esterni
384 righe: rilevamento per regole di dominio, rel corretto, icona e arricchimento automatico dell'aria-label con si apre in una nuova scheda, con un'euristica che evita l'annuncio doppio se il testo del link già lo dice. Agisce su tre punti (salvataggio ACF, componenti PHP, output buffer per i contenuti legacy), così nessuna via d'ingresso resta scoperta.
Skip link bilingui
In testa alla pagina, verso il contenuto principale e verso la ricerca, realizzati in flusso con altezza collassata invece che in posizionamento assoluto: restano invisibili finché non ricevono il focus, senza rischiare di coprire contenuto.
Qualità: tre round di audit su 324 segnalazioni
Il design è stato sottoposto a un audit esterno di conformità pixel-perfect, con le segnalazioni tracciate come commenti puntuali sul file Figma. Tre round, 324 segnalazioni complessive. Il volume rendeva impraticabile la lavorazione manuale una per una, così ho costruito un processo.
Estrazione automatica
Segnalazioni estratte via API Figma ed esportate in CSV: id, componente, testo, autore, stato, nodo di riferimento.
Raggruppamento per componente
Non per ordine di apparizione ma per componente: tutte le segnalazioni su Image Card insieme, tutte quelle su Header insieme. Un solo contesto mentale per volta, un commit per componente.
Verifica preventiva
Ogni segnalazione confrontata con il codice aggiornato: una parte era già stata risolta da altri merge nel frattempo, e correggere ciò che è già corretto è il modo più efficace per introdurre regressioni.
Valori esatti dalla libreria
Valori presi dalla libreria Figma, non stimati dallo screenshot, con verifica del computed style nel browser dopo ogni fix.
Triage dei falsi difetti di codice
Le segnalazioni che erano in realtà errori di contenuto (una CTA in minuscolo, un campo compilato male) sono state restituite al team editoriale invece di essere forzate via CSS. Correggere il contenuto con il codice è il debito tecnico più subdolo che esista.
I fix sono tracciati nel codice: i commenti citano l'ID della segnalazione (QA ID 292, QA 192), così tra un anno si saprà perché quella riga esiste.
Su una segnalazione ho interpretato al contrario la richiesta e il cliente se n'è accorto. La correzione è stata immediata, ma la lezione è stata un'altra: su un audit di 324 punti la fiducia non si costruisce dicendo che è tutto a posto, ma dichiarando quali sono i tre punti che vale la pena ricontrollare.
Il progetto in numeri
| Voce | Valore |
|---|---|
| Durata | 24 settimane attive (gennaio-luglio 2026) |
| Codice sorgente | 36.066 righe (23.035 PHP, 6.808 CSS, 6.210 JS) |
| Pattern del page builder | 46 |
| Componenti riusabili | 25 |
| Moduli di servizio | 31 |
| Layout ACF su 4 campi flexible | 51, con zero file orfani |
| Campi ACF configurabili | circa 700 |
| Token di design | 44 @theme più 25 utility tipografiche |
| Dipendenze npm | 2 |
| Framework JavaScript | 0 |
| Segnalazioni di audit lavorate | 324 su 3 round |
| Contributo personale | 410 commit su 1.157 (35%) e 87 merge di integrazione su 173 (50%) |
Cosa mi porto dietro
La convenzione batte la configurazione, se la fai rispettare dal sistema. Far risolvere quattro artefatti da un solo nome sembrava fragile all'inizio: ha retto 51 layout, cinque sviluppatori e sei mesi senza una singola eccezione hardcoded, e ha regalato gratis il caricamento condizionale degli asset. Ha funzionato perché romperla si nota subito.
Le decisioni migliori tolgono opzioni. I breakpoint alias, i colori nominati per segmento commerciale, i prezzi risolti da codice prodotto: ognuna ha ridotto ciò che era possibile fare e ognuna ha eliminato un'intera categoria di errori. In un team che cambia composizione durante il progetto, ciò che non si può sbagliare vale più di ciò che è documentato.
L'astrazione va pagata quando serve, non prima. La product card è arrivata a 1.341 righe e tre livelli di parametrizzazione perché ogni livello è stato aggiunto quando un caso reale lo ha richiesto, e i pattern da 26 righe che la usano sono la prova che il costo è stato ripagato.
Metà del mio lavoro misurabile su questo progetto sono merge, non feature. Le fondazioni di gennaio (scaffolding, convenzioni, git flow, README) sono duecento righe scarse che hanno determinato la forma di 36.000: il codice che scrivi conta meno del codice che gli altri scriveranno dopo di te.
Ti serve un design system o un page builder su misura?
Se hai un design system da tradurre in codice con fedeltà pixel-perfect, o un sito in cui la redazione deve comporre pagine in autonomia senza sacrificare il controllo sul rendering, posso affiancarti con lo stesso approccio raccontato in questo caso studio: architettura a componenti, design system codificato e accessibilità trattata come vincolo di progetto.