Sito vetrina e blog di una consulente di carriera
Il sito pubblico della stessa consulente: servizi, prenotazione della prima chiamata, contatti e blog. Gli articoli li scrive nel gestionale.
- Ruolo
- Analisi, contenuti tecnici, sviluppo, SEO, infrastruttura — autore unico
- Periodo
- luglio 2026 → oggi
- Stato
- Online da luglio 2026
- Pubblicato
- Autore
- Elia Raguso
- Online
- elisabettacarrara.com
- Angular 22
- Prerender SSG
- Cloudflare Pages
- Pages Functions
- Turnstile
- Resend
In breve
elisabettacarrara.com è il sito pubblico di Elisabetta Carrara, consulente di carriera: presentazione, servizi, prenotazione della prima chiamata, form di contatto e blog. È online da luglio 2026 su Cloudflare Pages.
Su mobile, PageSpeed Insights dà 99 di Performance, 100 di Accessibilità, 100 di Best
Practice, 100 di SEO, con CLS a 0. Il CLS era 0,201: si è azzerato con il preload dei
font critici e font-display: optional sui subset latini, tenendo swap su quelli
estesi.
Gli articoli del blog si scrivono nel gestionale della stessa consulente.
Nessun server a runtime
Ogni rotta è prerenderizzata, articoli compresi. Il sito è HTML statico su CDN, e le uniche parti dinamiche sono tre funzioni serverless (contatti, health check, webhook delle notifiche di consegna email).
La build però è configurata in modalità server, con tutte le rotte in prerender. Una rotta lasciata fuori dal prerender verrebbe passata a un server che su un hosting statico non esiste: la build passerebbe e in produzione quella pagina darebbe 404. L'avviso è scritto nel file delle rotte, accanto alla configurazione che lo rende possibile.
L'ottimizzazione del CSS critico è disattivata di proposito. Angular la implementa con
un onload inline sul foglio di stile, cioè uno script inline, che la Content Security
Policy del sito blocca senza segnalarlo. Il risultato è una pagina senza stile, con il CSS
rimasto in media="print". Un commento nel codice dice di non riattivarla senza rivedere
la CSP.
Il blog si scrive nel gestionale
Gli articoli non sono file nel repository: si scrivono nel gestionale, in un editor Markdown, e la pubblicazione chiama un deploy hook che fa ripartire la build del sito. La build interroga l'API, scarica gli articoli e le immagini, rende il Markdown in HTML e prerenderizza ogni pagina.
La pipeline è fail-closed: se un dato non è valido, la build si ferma e non pubblica niente. Si fermano, con codice di uscita 1: slug duplicato, chiave di front matter sconosciuta, corpo vuoto, un titolo di livello 1 dentro l'articolo (spaccherebbe la gerarchia dei titoli della pagina), data non ISO, campo oltre i limiti che l'API stessa impone, copertina mancante, immagine che dopo il rendering punta fuori origine, download di un'immagine fallito, API irraggiungibile o che risponde qualcosa che non è JSON.
Unica eccezione: una meta description troppo lunga dà un avviso, non un errore, così un titolo lungo scritto nel CMS non blocca la pubblicazione degli altri articoli.
La parte più delicata è il contratto sulla forma della risposta API: se un campo viene
rinominato lato server, non c'è nessun errore, il campo arriva undefined.
// ⚠ I nomi sono in camelCase: ASP.NET Core serializza coi default «web», quindi
// `PublishedAt` esce `publishedAt`. Un contratto in PascalCase troverebbe *tutti* i
// campi mancanti.
export const CAMPI_API = [
{ nome: 'slug', tipo: 'string', nullable: false },
{ nome: 'excerpt', tipo: 'string', nullable: true },
{ nome: 'coverImageUrl', tipo: 'string', nullable: false },
{ nome: 'publishedAt', tipo: 'data', nullable: false },
// …
];
Lo stesso elenco ha due consumatori in due momenti diversi: la build lo verifica sui dati
veri (ed è l'unica cosa che impedisce davvero un deploy sbagliato), e un test lo verifica
su una risposta registrata, senza rete. Il test simula il rename di ogni campo del DTO,
uno per uno. Prima che esistesse, il primo sintomo di un rename era un Invalid time value
sulla data — oppure, peggio, un articolo online senza estratto e senza meta description,
con la build verde.
Il form di contatto
L'endpoint dei contatti è una funzione serverless. I controlli, nell'ordine in cui vengono fatti:
| # | Controllo | Esito |
|---|---|---|
| 1 | variabili d'ambiente incomplete, o nessun destinatario valido | 500, fail closed |
| 2 | origine non ammessa (Origin, con Referer come ripiego) |
403 |
| 3 | Content-Type diverso da JSON |
415 |
| 4 | corpo oltre il limite, dichiarato e misurato | 413 |
| 5 | JSON non valido | 400 |
| 6 | honeypot compilato | 200 finto, niente invio |
| 7 | token Turnstile assente, troppo lungo, o non valido lato server | 403 |
| 8 | oltre 5 invii nell'ora dallo stesso IP | 429 |
| 9 | nome, email o messaggio non validi | 422 |
Il honeypot risponde 200, perché un bot che riceve un errore capisce di essere stato scartato e riprova in un altro modo. Il rate limit sta dopo Turnstile, per non consumare scritture sullo store distribuito con il traffico dei bot. La risposta contiene solo l'esito, senza dire quale controllo è fallito: i dettagli restano nei log, e chi sonda l'endpoint non scopre quali difese ci sono.
Il form manda anche la pagina di provenienza, cioè da dove è partito l'invio. È letta dalla navigazione interna, con il referrer come ripiego solo se è dello stesso sito, e ricontrollata lato server; nessun cookie, nessun tracciamento. Serve a sapere da quali pagine arrivano i contatti, senza analytics che profilano chi visita.
SEO: i numeri vengono da una sola fonte
I dati strutturati della home sono un @graph con il servizio professionale, la persona e
il sito. Il catalogo delle offerte è derivato dal listino che la pagina già mostra,
quindi i prezzi a schermo e quelli dichiarati a Google non possono divergere. Ogni
articolo emette il proprio BlogPosting con dateModified preso dalla data di
aggiornamento reale, non ricopiato dalla data di pubblicazione.
Due correzioni arrivate dalla Search Console: il canonical forza la barra finale,
perché è la forma che l'hosting serve con 200 (l'altra è un 308, e un canonical che punta a
un redirect è uno spreco); e il blocco degli articoli nella sitemap è rigenerato dalla
build solo dentro i suoi marcatori, così le quattro pagine fisse mantengono il loro
lastmod reale e non prendono la data di build.
Accessibilità: contrasti misurati
La palette viene dai colori del brand, e due non passavano. Il corallo dei pulsanti è stato
scurito perché quello originale non arriva a 4,5:1. Il testo attenuato era un'opacità:
opacity: .75 misurava 4,3:1 su crema e 4,7:1 su bianco, quindi lo stesso grigio passava o
no a seconda dello sfondo. Ora è un colore a sé, con il rapporto misurato scritto accanto.
L'anello di focus è 3 px del colore dei pulsanti più un alone bianco di 5 px. Il verde salvia dei bordi stava a 2,6:1, sotto il minimo di 3:1 per i componenti, e il solo colore dei pulsanti sarebbe sceso a 1,8:1 sui fondi navy.