Da Create React App ad Astro: come ho riscritto il mio portfolio senza perdere una riga di React
Perché ho abbandonato CRA dopo anni, cosa significa scegliere Astro per un progetto React esistente, e i problemi concreti che ho risolto lungo la strada: SVG nel workerd di Cloudflare, islands architecture, alias Vite.
Il mio portfolio era un’app React classica, costruita con Create React App anni fa. Funzionava. Non era veloce, non aveva SEO, non aveva blog — ma funzionava. Quando ho deciso di aggiungere un blog con articoli tecnici, mi sono trovato davanti a una scelta: continuare ad aggiungere complessità sopra CRA oppure cambiare le fondamenta.
Ho scelto di cambiare le fondamenta.
Perché non CRA
Create React App nasce come strumento didattico e per prototipi. Nel 2024/2025 è de facto deprecato — il repository ufficiale non riceve aggiornamenti, la community si è spostata su Vite, Next.js, o framework più moderni. Usarlo per un sito personale che vuole anche generare pagine statiche e ospitare contenuto MDX significa lavorare contro il tool, non con esso.
Le alternative erano due: Next.js o Astro.
Perché Astro e non Next.js
Next.js sarebbe stato ovvio — lo uso ogni giorno a lavoro. Ma il mio portfolio non ha bisogno di SSR, API routes, o edge functions per ogni pagina. Ha bisogno di:
- Un’app React interattiva (il desktop Web OS) che gira solo nel browser
- Pagine statiche per il blog (HTML generato a build time)
- Nessun JavaScript superfluo dove non serve
Astro è costruito esattamente per questo. Il suo modello è zero JS di default: ogni pagina è HTML statico a meno che tu non dichiari esplicitamente il contrario. React diventa un’isola — un componente che si idrata solo dove e quando serve.
L’architettura: React come isola
La parte più importante della migrazione è stata capire il concetto di Astro island. La mia app React (tutta la simulazione del desktop OS) vive in un singolo componente OsInterface. In Astro, questo diventa:
---
import OsInterface from '../components/OsInterface';
---
<OsInterface client:only="react" />
La direttiva client:only="react" significa: non renderizzare questo componente lato server, caricalo e idratalo solo nel browser. È la scelta giusta per un’app che usa window, document, localStorage — cose che non esistono in un contesto Node.js.
La prima versione usava client:load, che invece pre-renderizza lato server. Il risultato era un errore React 19 poco leggibile: gli import SVG restituivano oggetti non-stringa in SSR, e React rifiutava di passarli come src a un <img>.
Cambiare client:load in client:only="react" ha risolto l’errore in una riga.
Il problema degli SVG nel Cloudflare adapter
Con client:only l’errore React scompariva, ma le icone restavano invisibili. Dev server pulito, nessun errore in console — le SVG semplicemente non si caricavano.
Il problema era nel Cloudflare adapter.
Astro con @astrojs/cloudflare fa girare il dev server in un ambiente workerd — il runtime di Cloudflare Workers — invece del classico Node.js. In questo ambiente, i file importati da Vite ricevono URL del tipo /@fs/percorso/assoluto/file.svg. Questi URL funzionano nel contesto Vite normale, ma il workerd non li serve correttamente.
La soluzione è stata spostare tutti gli asset statici nella cartella public/. I file in public/ vengono serviti direttamente come file statici a URL assoluti (/assets/svg/logo.svg) — nessun passaggio attraverso la pipeline Vite, nessun problema con il runtime.
// Prima — import Vite, rotto nel workerd
import Logo from '../../../assets/svg/logo.svg';
// Dopo — URL assoluto, funziona ovunque
const Logo = '/assets/svg/logo.svg';
Ho applicato questa trasformazione a tutti i componenti che importavano SVG o PDF.
Gli alias Vite
CRA usava tsconfig.json con baseUrl: "src" per risolvere import assoluti (import X from 'utils/isMobile'). Vite supporta la stessa cosa, ma in Astro l’approccio corretto è dichiarare gli alias esplicitamente in astro.config.mjs:
vite: {
resolve: {
alias: {
App: path.join(srcDir, 'App'),
components: path.join(srcDir, 'components'),
config: path.join(srcDir, 'config'),
// ...
},
},
},
Questo evita di affidarsi a vite-tsconfig-paths (un plugin aggiuntivo) e rende gli alias espliciti e controllati.
Il blog MDX
Una volta sistemata la parte React, aggiungere il blog è stato sorprendentemente semplice. Astro ha un sistema di content collections integrato:
// src/content.config.ts
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
tags: z.array(z.string()).optional(),
lang: z.enum(['it', 'en']).default('en'),
translationSlug: z.string().optional(),
}),
});
Ogni file .mdx in src/content/blog/ diventa automaticamente un articolo tipizzato. La pagina [slug].astro recupera il contenuto e lo renderizza:
---
const { post } = Astro.props;
const { Content } = await render(post);
---
<Content />
Niente configurazione webpack, niente plugin babel, niente magia nascosta.
Risultato
La struttura finale:
/— app React completa, caricata come isola conclient:only="react"/blog— lista articoli statica, zero JavaScript/blog/[slug]— articolo MDX renderizzato a HTML statico
La build gira su Cloudflare Pages con il loro adapter statico. Tempi di build inferiori a 30 secondi, distribuzione istantanea sulla CDN globale.
L’intera migrazione ha mantenuto il codice React invariato. Zero componenti riscritti, zero Redux refactoring. Astro si è limitato a fare il suo lavoro: coordinare le parti statiche e lasciare React gestire quelle interattive.
A volte la scelta giusta non è riscrivere tutto. È scegliere il tool giusto per il layer giusto.