Astro, Ghost o WordPress per un blog tecnico? Come (e perché) ho scelto Markdown
Astro vs Ghost vs WordPress: il percorso decisionale onesto per scegliere lo stack di un blog tecnico. Performance, costi, CMS a confronto. Alla fine ho scelto Astro + Markdown + Cloudflare Pages. Costo: 13€/anno.
Stavo per fare overengineering sul mio blog. Di nuovo.
Ghost su Docker come headless CMS, Astro come frontend, webhook per il rebuild, reverse proxy con OpenResty. Un’architettura da startup SaaS per un blog personale che, siamo onesti, pubblicherà due post al mese.
Poi mi sono fermato, ho guardato il docker-compose e mi sono chiesto: ma perché mantenere due container per qualcosa che posso fare con dei file .md in un repo Git?
Ecco il percorso completo, con tutti i ripensamenti.
Il contesto
Gestisco infrastrutture server per lavoro. So cosa significa mantenere WordPress con 18.000 articoli, WPML che esplode, database da 2.5GB, Redis, CrowdSec, backup. So anche cosa significa quando un container si rompe alle 3 di notte.
Per il mio blog personale volevo esattamente l’opposto: zero manutenzione, zero container, zero database. Performance perfette di default.
Fase 1. Quanto sono veloci i framework moderni?
Prima di scegliere, ho guardato i numeri. I dati HTTP Archive e Chrome UX Report del 2025 parlano chiaro:
Il 60% dei siti costruiti con Astro raggiunge una valutazione “Good” sui Core Web Vitals. WordPress è al 38%, Gatsby al 45%. Astro risulta circa il 40% più veloce dei framework basati su React, con il 90% in meno di JavaScript inviato al browser grazie alla Islands Architecture.
Tradotto: un sito Astro statico puro, senza analytics né script di terze parti, fa Lighthouse 95-100/100 senza nessuno sforzo. Aggiungi analytics, cookie consent e font custom: la storia si complica un po’, ma la base di partenza è eccellente.
La scelta del framework era fatta. Restava il CMS.
Confronto CMS: Ghost vs WordPress vs Storyblok vs Markdown
Ho valutato quattro opzioni serie. Le riassumo per chi sta facendo la stessa scelta.
Ghost è il più elegante. Editor bellissimo, newsletter nativa, Content API pulita, self-hosted gratuito. Il problema? Zero campi custom, zero contenuti strutturati. Funziona perfettamente come blog/newsletter, meno bene per tutto il resto.
WordPress lo conosco fin troppo bene. Ecosistema plugin infinito, familiare. Ma pesante, superficie d’attacco enorme, manutenzione costante. Per un blog personale nel 2026 è overkill.
Storyblok è interessante se hai bisogno di un editor visual e campi custom. Ma è SaaS, il costo scala con l’uso, e per un blog personale è troppo.
Markdown in Astro, zero infrastruttura, zero manutenzione, zero costo. Editing in un text editor. Per un blog tecnico dove scrivi code block ogni tre paragrafi, è il formato naturale.
Fase 3. La prima decisione (sbagliata)
La prima architettura era questa:
- Ghost su Docker (sulla VPS che uso per altri progetti) come CMS headless
- Astro come frontend su Cloudflare Pages
- OpenResty come reverse proxy per l’admin Ghost
- Webhook Ghost → Cloudflare per rebuild automatico a ogni pubblicazione
L’idea era buona: scrivo su Ghost con il suo editor fantastico, il frontend statico viene ricostruito automaticamente. Il meglio dei due mondi.
Ho persino iniziato a configurare il docker-compose.yml:
services:
ghost:
image: ghost:5-alpine
restart: always
ports:
- "127.0.0.1:2368:2368"
environment:
url: https://alessandrodecenzo.it
database__client: mysql
database__connection__host: ghost-db
# ...
volumes:
- ghost-content:/var/lib/ghost/content
ghost-db:
image: mysql:8.0
restart: always
volumes:
- ghost-db:/var/lib/mysql
Due container, un database MySQL, volumi persistenti, backup da gestire. Per un blog.
Fase 4. Il ripensamento
Il trigger è stato semplice: ho deciso che non mi serviva una newsletter. Almeno non subito.
Senza newsletter, Ghost perde il suo vantaggio killer. Diventa un editor Markdown con API, e io sto mantenendo due container Docker per qualcosa che posso fare con dei file .md in una cartella.
Il calcolo era ovvio:
Ghost headless significava 2 container sulla VPS, backup database, aggiornamenti Ghost, reverse proxy da configurare, webhook da mantenere, e circa 20€/mese di risorse server.
Markdown nel repo significava zero container, zero backup (Git è già il backup), zero manutenzione, zero costo infrastruttura. Solo 13€/anno per il dominio.
Fase 5. La decisione finale
Lo stack definitivo:
- Content: file
.mdnel repo GitHub, con Astro 5 Content Collections - Build: Astro 5 (static site generation)
- Deploy: Cloudflare Pages, free tier, edge globale, ~50ms TTFB
- Dominio: alessandrodecenzo.it su Keliweb
Il workflow è: scrivo un .md, pusho su main, Cloudflare Pages fa il build e deploy automatico. Fine.
---
title: "Il mio primo post"
description: "Descrizione per SEO e social"
date: 2026-02-25
tags: ["astro", "blog"]
---
Contenuto in Markdown. Code block, immagini, tutto supportato.
Costo totale: ~13€/anno. Solo il dominio.
Fase 6. Il design
Volevo un sito che comunicasse “developer, terminale, tecnico” a colpo d’occhio. Il contrario del sito dell’agenzia (che è light-first, minimal corporate).
Palette dark-first. Sfondo #141414, card #1c1c1c, code blocks #0e0e0e. Rosso accent in due varianti: #F05050 per il testo su sfondi scuri (link, tag, evidenziazioni) e #D4162C per gli sfondi dei bottoni con testo bianco sopra.
Il problema dell’accessibilità. Ho scritto uno script Python per verificare il contrasto WCAG AA su tutte le combinazioni colore della palette:
import itertools
def relative_luminance(hex_color):
"""Calcola la luminanza relativa di un colore HEX."""
r, g, b = int(hex_color[1:3], 16), int(hex_color[3:5], 16), int(hex_color[5:7], 16)
rgb = []
for c in [r, g, b]:
c = c / 255.0
c = c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
rgb.append(c)
return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]
def contrast_ratio(hex1, hex2):
"""Calcola il rapporto di contrasto tra due colori."""
l1 = relative_luminance(hex1)
l2 = relative_luminance(hex2)
lighter = max(l1, l2)
darker = min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
# Palette completa
backgrounds = {
"bg-code": "#0e0e0e",
"bg-primary": "#141414",
"bg-surface": "#1c1c1c",
"bg-inline": "#252525",
}
texts = {
"text-bright": "#eeeeee",
"text-primary": "#d4d4d4",
"text-secondary": "#909090",
"text-muted": "#848484",
"accent": "#F05050",
"green": "#5ec269",
}
# Verifica tutte le combinazioni
for (bg_name, bg_hex), (txt_name, txt_hex) in itertools.product(
backgrounds.items(), texts.items()
):
ratio = contrast_ratio(bg_hex, txt_hex)
status = "PASS" if ratio >= 4.5 else "FAIL (AA)"
# AA per testo grande: >= 3.0, per testo normale: >= 4.5
print(f"{txt_name} on {bg_name}: {ratio:.2f}:1 [{status}]")
Risultato: 9 combinazioni su circa 24 non passavano WCAG AA nella prima iterazione. Il fix principale è stato introdurre il doppio livello di rosso: #F05050 (più chiaro, 4.84:1 minimo sui fondi scuri) per il testo e #D4162C (l’originale _blank) solo per i bottoni dove il testo sopra è bianco.
Typography. JetBrains Mono per headings, code e navigazione, dà il feel “terminale” a tutta l’interfaccia. Inter per il body text, perché la leggibilità su long-form è eccellente.
Code blocks. Niente pallini rosso/giallo/verde stile macOS (è un cliché). Header stile terminale con path del file e tag del linguaggio.
Performance Astro: i numeri reali (Lighthouse, Core Web Vitals)
Un sito Astro puro — zero analytics, zero script di terze parti — fa Lighthouse 95-100/100 senza nessun lavoro. Zero JavaScript nel bundle, HTML statico servito dall’edge di Cloudflare: FCP sotto i 500ms, TBT 0ms.
Questo sito ha in più: Google Analytics 4 (via GTM), cookie consent GDPR, font self-hosted. Ha senso misurare con queste cose attive, è la configurazione reale di qualsiasi blog professionale.
Risultato finale, connessione 4G simulata (Lighthouse 13):
| Metrica | Valore | Score |
|---|---|---|
| First Contentful Paint | 2.0s | — |
| Largest Contentful Paint | 2.0s | — |
| Total Blocking Time | 0ms | 1.0 ✓ |
| Cumulative Layout Shift | 0 | 1.0 ✓ |
| Speed Index | 2.1s | — |
| Performance totale | — | 98/100 |
Tre run consecutive, 97-98/100. Per arrivare qui ho fatto quattro ottimizzazioni specifiche che vale la pena documentare.
1. Self-hosting dei font. Partivo con Google Fonts: 815ms di CSS render-blocking, due font swap separati (Inter body + JetBrains Mono headings) che causavano CLS visibile a ogni cold visit. Fix: @fontsource/inter e @fontsource/jetbrains-mono installati come dipendenze npm, .woff2 copiati in public/fonts/, font-display: optional nel CSS. Niente preload: su connessione 4G simulata i preload font competono con il CSS per la banda disponibile e peggiorano FCP. Con font-display: optional i font arrivano dalla cache dal secondo visit in poi, senza bisogno di preload. Zero CLS da font, zero request esterne, zero render-blocking.
2. CLS del cookie banner. vanilla-cookieconsent v3 in modalità layout: 'box inline' si inserisce nel flusso della pagina e sposta il contenuto verso il basso alla comparsa: CLS 0.188, il valore più alto misurato. Un parametro, risolto: layout: 'box' usa un overlay fisso che non tocca il layout.
3. GTM lazy load. Google Tag Manager carica solo alla prima interazione utente (scroll, click, touchstart, keydown). GTM e GA4 non toccano il main thread durante il caricamento iniziale della pagina: nessun render-blocking, nessun TBT da analytics. Ho provato anche Cloudflare Zaraz (tag gateway server-side) come alternativa: in teoria migliore perché gestisce i tag lato server, in pratica il suo loader JavaScript pesava ~350KB più ~475KB di GA4 proxyato, caricati subito al page load. Risultato: TBT da 260ms a 730ms, score da 93 a 68. Il GTM lazy-load su interazione vince perché Lighthouse non genera interazioni utente e non vede mai GTM.
4. Dimensioni esplicite per media esterni. Il badge Green Web Foundation nel footer aveva height: auto nel CSS che sovrascriveva l’attributo HTML height="57", impedendo al browser di riservare lo spazio prima del caricamento dell’immagine. Fix: width: 120px; height: 57px espliciti nel CSS. Stessa logica per il widget Tree-Nation (min-width: 90px; min-height: 40px).
Il risultato di partenza, prima di queste ottimizzazioni, era 71/100. Confronto stack con configurazione analoga:
| Metrica | Astro (questo sito) | WordPress ottimizzato | Ghost |
|---|---|---|---|
| Lighthouse Performance | 98/100 | 80-90 | 85-92 |
| TTFB | ~50ms | 200-500ms | 100-200ms |
| Cumulative Layout Shift | 0 | variabile | variabile |
| JS bundle proprio | ~0 KB | 200-500 KB | 50-100 KB |
| Costo hosting | €13/anno | €5-20/mese | €5-15/mese |
Limiti di Markdown puro e quando scegliere Ghost o WordPress
Markdown puro non è per tutti. Scrivi in un text editor, non hai preview live nel browser (a meno di tenere npm run dev attivo), non hai un pannello con bottoni per grassetto e corsivo. Se non ti è naturale scrivere in Markdown, è una scelta sbagliata. Alternative come TinaCMS o Decap CMS possono aggiungere un editor visual sopra i file Markdown nel repo, il meglio dei due mondi, se ne hai bisogno.
Se servisse la newsletter, dovrei aggiungere un servizio esterno (Buttondown, ConvertKit, o Mailchimp). È il tradeoff di aver tolto Ghost: ho semplificato l’infrastruttura ma ho perso la newsletter integrata.
Il “deployment via Git push” è fantastico se sei un developer. Se dovessi far scrivere un non-tecnico su questo blog, Ghost o WordPress sarebbero scelte migliori.
Ma per un blog tecnico personale dove il 50% del contenuto è codice? File Markdown nel repo è la risposta più onesta.