Fractal e personalizzazioni

Cos’è Fractal

Fractal è uno strumento open source per costruire style guide e librerie di componenti: prende i template sorgente di un progetto (in RTDS, file Nunjucks .njk), li renderizza con dati di esempio e li espone in un’interfaccia web navigabile - una sidebar con tutti i componenti, un’anteprima interattiva per ciascuno, il markup HTML generato, e una sezione di documentazione libera in Markdown a fianco.

RTDS lo usa come unica fonte della UI di consultazione: sia la doc dei componenti (ogni pagina che si consulta in questo sito), sia le pagine di testo come questa, sono generate da Fractal. Non è un CMS: i contenuti sono file versionati nel repository, Fractal si limita a renderizzarli.

Cosa fa Fractal e cosa no. Fractal renderizza i template .njk e genera la UI di consultazione (sidebar, anteprime, questa stessa documentazione) - non compila CSS o JavaScript: per queste attività delega a strumenti di build esterni, integrabili tramite la sua API. In RTDS quel lavoro è affidato a Webpack, con una configurazione specifica del progetto: legge i CSS sorgente (assets/css/), li elabora con Tailwind CSS e PostCSS (autoprefixer incluso) e produce i CSS pubblicati in public/css/; allo stesso tempo raggruppa il JavaScript dei singoli componenti in un unico bundle (componentsJs.js) - un’aggregazione non prevista da Fractal, ma configurata ad hoc per RTDS. In sviluppo, npm run start avvia Fractal e Webpack in parallelo (insieme a un watcher dedicato ai CSS dei componenti): l’anteprima si aggiorna automaticamente perché i due processi lavorano in contemporanea, ma restano strumenti distinti con responsabilità distinte. In build di produzione la sequenza è invece esplicita: prima Webpack compila gli asset, poi Fractal genera la UI statica che li referenzia già pronti.

Le due istanze RTDS

RTDS non gira su una singola istanza Fractal, ma su due, con perimetri di componenti diversi:

  • Fractal Core - atomi, molecole, organismi, template e i componenti del Portale Istituzionale.
  • Fractal Servizi - widget e componenti applicativi per i portali servizi.

Il quadro completo (contenuto, dev server, cartelle di build) è descritto in Struttura delle cartelle e nelle due pagine “Organizzazione componenti”, che chiudono ciascuna con la relazione verso l’altra libreria. Questa pagina non ripete quel contenuto: si concentra su Fractal come strumento e su cosa RTDS ha personalizzato rispetto al comportamento di base.

Personalizzazioni visibili nella UI

Alcuni aspetti dell’interfaccia non sono il comportamento predefinito di Fractal, ma scelte fatte per RTDS:

Tema grafico brandizzato. L’interfaccia (sidebar, tipografia, colori) usa il tema @frctl/mandelbrot, personalizzato con l’identità visiva di Regione Toscana - non è l’aspetto di default che si otterrebbe installando Fractal senza configurazione.

Sidebar componenti filtrata per istanza. Ogni istanza mostra solo il proprio perimetro: Fractal Core nasconde le collezioni widgets/servizi/templates-servizi, Fractal Servizi nasconde tutte le collezioni “core” (atomi, molecole, organismi, template, portale istituzionale) mostrando solo token e primitivi, widget e servizi. Una categoria assente dalla sidebar di un’istanza è esclusa da questo filtro: i componenti restano nel codice sorgente condiviso, ma non sono navigabili da quell’istanza.

Sidebar documentazione filtrata per istanza. Allo stesso modo, alcune pagine di questa sezione “documentazione” sono nascoste in un’istanza e visibili nell’altra: “Libreria Core” è visibile solo da Core, “Libreria Servizi” solo da Servizi, mentre “Presentazione”, “Installazione e uso” e “Fondamenta” sono condivise.

Resource browser filtrato. Aprendo la scheda risorse di un componente si vedono solo gli asset reali (template, CSS, JS, config) e non i file di predisposizione interna usati per generare Storybook, esclusi deliberatamente dalla vista.

Alcuni link di questa documentazione (in particolare quelli che puntano da un’istanza all’altra, o tra pagine di documentazione) cambiano in base all’ambiente in cui la si consulta: in locale durante lo sviluppo puntano al server di sviluppo dell’altra istanza (es. un indirizzo localhost), nella build pubblicata puntano al percorso relativo corretto. L’adattamento è automatico e non richiede riscritture manuali.


Note tecniche di manutenzione

Da qui in poi la pagina si rivolge a chi lavora sul repository RTDS. Per consultare la libreria o integrarla in un progetto non serve leggere oltre: quanto segue riguarda la manutenzione della configurazione Fractal.

I sei punti seguenti condividono la stessa caratteristica: non producono un errore né un warning quando la configurazione è sbagliata - l’unico segnale è un output diverso da quello atteso. Fa eccezione il punto 6, che in un caso specifico interrompe il rendering dell’intera pagina.

1. Risoluzione varianti per nome file

Fractal cerca un file component--variantname.njk per ogni variante dichiarata nel .config.yml di un componente. Se il file non esiste, ricade silenziosamente sul template base component.njk, senza segnalare nulla.

Esempio reale - components/02-atoms/spinner/:

spinner.njk                       ← template base
spinner--with-label.njk           ← variante con label sr-only
spinner--with-label-heading.njk   ← variante con label visibile <h2>

Come evitarlo: quando si aggiunge al .config.yml una variante che richiede markup diverso dal default, creare sempre il file component--variantname.njk corrispondente. Impostare solo parametri di contesto non basta se il template base non li usa nel modo corretto - e non c’è alcun avviso in fase di build se il file manca.

2. resources è un overwrite completo, non un merge

fractal.components.set('resources', ...) sostituisce interamente la configurazione di default del resource browser, non la integra. Il default nativo di Fractal è { assets: { match: ['**/*'] } } (mostra tutti i file del componente).

RTDS lo personalizza per nascondere i file di predisposizione Storybook, ma usando un pattern di negazione nello stesso gruppo match, non una whitelist di estensioni:

// fractal.config.js:39-44 e fractal.servizi.config.js:39-44 (identico in entrambi)
fractal.components.set('resources', {
    assets: {
        match: ['**/*', '!**/*.stories.js'],
        label: 'Assets',
    },
});

Perché è un problema: sostituire questo blocco con una whitelist (es. { css: { match: '**/*.css' } }) nasconde silenziosamente ogni file non esplicitamente elencato - inclusi i .js reali dei componenti, non solo quelli che si intendeva escludere. anymatch (il matcher usato internamente da Fractal) valuta i pattern ! prima di quelli positivi, quindi il modo corretto di esprimere “tutto tranne X” è ['**/*', '!X'], non “solo Y”.

Come evitarlo: qualunque modifica a questo blocco va pensata in termini di “tutto tranne X”, e va applicata a entrambi i config (fractal.config.js e fractal.servizi.config.js condividono lo stesso blocco, non è sufficiente correggerne uno solo).

3. Nascondere una pagina docs la esclude anche dalla build statica

Impostare item._isHidden = true su una pagina di documentazione (fractal.docs.items()) la esclude anche dalla build statica in dist/, non solo dalla sidebar - a differenza delle pagine componente (fractal.components.items()), dove nascondere dalla nav non ne impedisce l’esportazione in build.

Le liste attuali:

// fractal.config.js:137 - CORE_DOCS_HIDE
['servizi', 'superpowers', 'ai-work-tracker', 'index']

// fractal.servizi.config.js:112 - SERVIZI_DOCS_HIDE
['core', 'superpowers', 'ai-work-tracker', 'index']

Perché è un problema: il builder statico di Mandelbrot filtra esplicitamente !isHidden sulle pagine docs, ma non applica lo stesso filtro alle pagine componente - è facile assumere (sbagliando) che i due comportamenti siano simmetrici.

Come evitarlo: prima di nascondere una nuova pagina docs dal nav, non assumere che resti comunque raggiungibile in build statica via URL diretto - verificarlo esplicitamente rigenerando dist/. Se serve che una pagina resti sia nascosta dal nav sia presente in build, l’unica strada è spostarla fuori da docs/ a monte, non affidarsi a _isHidden.

4. Il global path() di Mandelbrot restituisce il file, non la directory

Il global Nunjucks path() esposto dal tema @frctl/mandelbrot (usato per calcolare link relativi robusti a qualunque profondità di pagina) restituisce il percorso relativo al file index.html della root dell’istanza, non alla directory radice.

Esempio reale - theme-overrides/views/partials/header.nunj:13-15, usato per il link cross-ambiente Core/Servizi in testata:

{% set rootPath = path('/') %}
{% set rootDir = rootPath.slice(0, rootPath.lastIndexOf('/') + 1) %}
{% set crossEnvHref = rootDir + '../' + frctl.theme.get('crossEnvTarget') + '/' %}

Perché è un problema: concatenare direttamente path('/') + '../altra-cartella/' produce un href malformato (il nome del file resta nella stringa, es. ../../index.html../altra-cartella/). L’errore non emerge testando solo la homepage (dove il file è nella stessa directory della root), ma diventa evidente su pagine a profondità maggiore (dettaglio componente, docs annidate).

Come evitarlo: quando si usa path() per costruire un link verso una cartella sorella della root (non verso un’altra pagina interna), estrarre prima la directory come nell’esempio sopra, e verificare sempre l’output a più profondità di pagina - non solo sulla homepage, dove il bug è invisibile.

5. I README dei componenti non passano dal motore Nunjucks dei docs

fractal.docs.engine(nunjucks({ globals: {...} })) registra i global (docExt, serviziUrl/coreUrl, serviziUrlRoot/coreUrlRoot) solo per l’engine che processa i file dentro docs/. I README.md dei singoli componenti (components/*/README.md), mostrati nella tab Notes dell’interfaccia Fractal, sono renderizzati da un percorso diverso (component notes), che non ha questi global disponibili.

Perché è un problema: usare {{ docExt }} o un altro global dei docs in un README component non produce un errore - il testo resta letterale nell’output ({{ docExt }} compare così com’è, invece di essere sostituito).

Come evitarlo: nei README dei componenti, per riferimenti incrociati (ad altri README, a pagine docs/*.md, ad altre sezioni della libreria):

  • non usare {{ docExt }} o altri global Nunjucks dei docs - non vengono sostituiti;
  • non calcolare a mano path relativi verso docs/ (la profondità di components/ varia da 2 a 4+ livelli a seconda della categoria, e non c’è comunque garanzia di routing per quel path nella tab Notes);
  • usare invece riferimenti testuali semplici (es. “vedi la pagina documentazione ‘Sprite SVG’ nella sezione Fondamenta”, o il nome del componente in backtick). I link a risorse statiche servite da public/ (es. /css/..., /js/...) restano invece validi con path assoluto, perché Fractal serve public/ alla root sia in dev sia in build statica.

6. Il motore Nunjucks dei docs esegue anche la sintassi mostrata dentro un code fence

Ogni pagina dentro docs/ passa dal motore Nunjucks prima di diventare HTML - questo vale per l’intero contenuto del file, non solo per il testo prosa. Un tag Nunjucks scritto dentro un code fence o un inline code Markdown, con l’intento di mostrarlo come esempio letterale, viene invece eseguito come un vero tag.

Perché è un problema: se il tag mostrato come esempio referenzia un global non disponibile in quel contesto (es. path() in un punto dove non è registrato), la pagina smette di renderizzare del tutto, con un errore a runtime tipo “Unable to call path, which is undefined or falsey” al posto del contenuto - o, se il tag ha una sintassi che il parser non riconosce (es. un placeholder generico non richiuso correttamente), un errore di parsing tipo “tag name expected” che blocca la pagina ancora prima del rendering. Se invece il tag referenzia un global che è disponibile (es. quello usato per l’estensione dei link tra pagine, registrato per l’intero engine docs/), non c’è errore: il tag viene silenziosamente valutato e sostituito con il suo valore reale, invece di comparire come testo letterale - l’esempio risulta comunque sbagliato, solo in modo meno visibile.

Come evitarlo: per mostrare sintassi Nunjucks letterale dentro una pagina docs/*.md, avvolgerla sempre nei tag di escape dedicati di Nunjucks (visibili in azione nell’esempio sotto), sia dentro un code fence sia dentro un inline code:

```nunjucks
{% set esempio = path('/') %}
```

Verificare sempre col dev server (npm run start) che la pagina non mostri il banner “Error rendering page” dopo aver aggiunto uno snippet con sintassi Nunjucks - l’assenza di errore da sola non basta se il tag referenzia un global disponibile (vedi sopra), va controllato anche che il testo compaia letterale e non sostituito.