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.

WordPress
PHP 8
ACF Extended
Tailwind CSS v4
JavaScript vanilla
Swiper 11
esbuild
Figma
Jira
Bitbucket
Tinexta InfoCert - design system e page builder proprietario

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.

Scheda progetto
AspettoDettaglio
RuoloLead developer e product owner tecnico: fondazioni, architettura, componenti verticali, QA e handover
PeriodoGennaio-luglio 2026, 24 settimane attive di sviluppo
Team4 sviluppatori in build, più il team interno del cliente in consolidamento
StackWordPress, PHP 8, ACF Extended, Tailwind CSS v4, JavaScript vanilla (ES Modules), Swiper 11
ToolingFigma, Jira, Bitbucket, esbuild per la minificazione
Codice36.066 righe (23.035 PHP, 6.808 CSS, 6.210 JS), zero framework front-end
VincoloFedeltà 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.

text
1template tmpl_flexible.php, single/tmpl_article.php
2 |
3dispatcher unico template-parts/flexible-loop.php (51 righe)
4 |
5pattern block-*.php (46 file) <- unico strato che legge ACF
6 |
7componenti includes/*.php (25 file, puri: ricevono solo $args)
8 |
9servizi includes/*.php (31 moduli: funzioni e hook WordPress)

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.

text
1layout "imageCard" -> template-parts/block-imageCard.php
2 -> assets/css/template-parts/block-imageCard.css
3 -> assets/js/template-parts/block-imageCard.js
4 -> 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.

css
1@utility h1 {
2 font-weight: 700; font-size: 28px; line-height: normal; letter-spacing: -0.462px;
3 @variant lg { font-size: 40px; line-height: 48px; letter-spacing: -0.408px; }
4}

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.

css
1--breakpoint-sm: 768px; --breakpoint-md: 768px; /* stesso valore */
2--breakpoint-lg: 1280px;
3--breakpoint-xl: 1440px; --breakpoint-2xl: 1440px; /* stesso valore */

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.

Tre livelli di parametrizzazione della product card
LivelloMeccanismo
Presenza delle sezioni6 flag booleani have_* che accendono o spengono intere parti (immagine, header, descrizioni, inclusioni, cross-selling, scadenza)
Modalità di renderingprice_only come uscita anticipata (solo prezzo e CTA), più pattern_type e price_layout con 4 valori ciascuno
Override di layoutclassi 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é.

text
1/* Gap del wrapper price_only tra prezzo e CTA commerciale: 12px di default,
2 * sovrascrivibile dal chiamante (es. boxed-section usa 20px da design). */

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.

html
1<div class="block-comparisonTable__inside-grid"
2 style="--comparison-columns: <?= esc_attr($columnsCount) ?>;">

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

Metriche di codice, contenuto e coordinamento
VoceValore
Durata24 settimane attive (gennaio-luglio 2026)
Codice sorgente36.066 righe (23.035 PHP, 6.808 CSS, 6.210 JS)
Pattern del page builder46
Componenti riusabili25
Moduli di servizio31
Layout ACF su 4 campi flexible51, con zero file orfani
Campi ACF configurabilicirca 700
Token di design44 @theme più 25 utility tipografiche
Dipendenze npm2
Framework JavaScript0
Segnalazioni di audit lavorate324 su 3 round
Contributo personale410 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.

Grazie per aver visionato questo progetto