02. Progetto, componenti e stili

← 01 - Fondamenta e modello mentale · Indice · successivo → 03 - Routing, pagine ed endpoint

2.1 Struttura di un progetto

public/            asset serviti così come sono (favicon, robots.txt, PDF)
src/
  pages/           OBBLIGATORIA - ogni file qui dentro è una rotta
  components/      convenzione
  layouts/         convenzione
  styles/          convenzione
  content/         convenzione (i tuoi .md), ma non è più obbligatoria
  content.config.ts   configurazione delle content collections
astro.config.mjs
tsconfig.json

Solo src/pages/ è imposta dal framework. Tutto il resto è convenzione e puoi riorganizzarlo.

public/ contro src/: la differenza non è cosmetica.

  • File in src/ → processati da Vite: hashati, ottimizzati, bundlati. Le immagini qui dentro passano per l’ottimizzatore.
  • File in public/ → copiati intatti, referenziati per path assoluto (/foo.pdf). Nessuna ottimizzazione, nessun cache-busting.

Regola pratica: immagini e asset che vuoi ottimizzati stanno in src/; PDF, robots.txt, favicon e file che devono avere un URL stabile stanno in public/.

Cambiato da Astro 4

src/content/ non è più una cartella riservata e src/content/config.ts non esiste più: la configurazione delle collection sta in src/content.config.ts e i contenuti possono stare dove vuoi. Vedi 04 - Contenuti - Markdown, MDX e content collections.

📖 Project structure

2.2 Anatomia di un componente .astro

Un file .astro ha due parti: il frontmatter (tra i ---) e il template.

---
// Questo codice gira SOLO sul server, a build time o a request time.
// Non finisce mai nel bundle del browser.
import Bottone from '../components/Bottone.astro';
 
interface Props {
  titolo: string;
  saluto?: string;
}
 
const { titolo, saluto = 'Ciao' } = Astro.props;
const dati = await fetch('https://api.example.com/cose').then((r) => r.json());
---
 
<h2>{saluto}, {titolo}!</h2>
<ul>
  {dati.map((d) => <li>{d.nome}</li>)}
</ul>
{dati.length > 0 && <p>Trovati {dati.length} elementi.</p>}
<Bottone />

Cose da notare:

  • await al livello superiore del frontmatter: è normale, non serve una funzione asincrona. Astro attende prima di renderizzare.
  • interface Props è la convenzione riconosciuta da astro check e dall’estensione VS Code. Non è obbligatoria, ma senza non hai autocompletamento.
  • Le espressioni {} funzionano come in JSX per interpolazione, .map() e condizionali (&&, ternario).

.astro non è JSX

AstroJSX
AttributiHTML standard: class, for, data-valueclassName, htmlFor
Elementi radice multipliconsentiti, niente wrapperserve un root o <>
Commenti<!-- HTML --> e {/* JS */}solo {/* */}

I commenti HTML finiscono nel DOM, quelli JS no. Dettaglio piccolo, utile quando lasci note nel template.

Tag dinamici: il nome della variabile deve iniziare per maiuscola.

---
const Elemento = 'div';
---
<Elemento>Ciao</Elemento>

Con la minuscola Astro lo tratta come tag HTML letterale <elemento>.

📖 Astro components · Astro syntax

2.3 Slot: come si compone

Lo <slot /> è il buco dove finiscono i figli del componente.

---
// src/components/Riquadro.astro
---
<div class="riquadro">
  <header><slot name="titolo" /></header>
  <div class="corpo">
    <slot>Contenuto di default se non passi nulla</slot>
  </div>
  <footer><slot name="note" /></footer>
</div>
<Riquadro>
  <h2 slot="titolo">Attenzione</h2>
  <p>Questo finisce nello slot di default.</p>
  <small slot="note">Nota a piè di riquadro.</small>
</Riquadro>

Due cose meno ovvie ma molto utili:

Slot condizionale: renderizzi il contenitore solo se lo slot è stato riempito:

{Astro.slots.has('note') && (
  <aside><slot name="note" /></aside>
)}

Slot passante: un layout che inoltra uno slot a un layout interno:

<LayoutBase>
  <slot name="head" slot="head" />
  <slot />
</LayoutBase>

Ricorsione con Astro.self: è come si costruisce un menu ad albero o una sidebar annidata:

---
// src/components/Menu.astro
type Voce = { titolo: string; href?: string; figli?: Voce[] };
interface Props { voci: Voce[] }
const { voci } = Astro.props;
---
<ul>
  {voci.map((v) => (
    <li>
      {v.href ? <a href={v.href}>{v.titolo}</a> : <span>{v.titolo}</span>}
      {v.figli && <Astro.self voci={v.figli} />}
    </li>
  ))}
</ul>

2.4 Layout

Un layout è solo un componente .astro che contiene <html> e uno <slot />. Non c’è magia.

---
// src/layouts/Base.astro
interface Props { titolo: string; descrizione?: string }
const { titolo, descrizione = 'Sito di esempio' } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site);
---
<!doctype html>
<html lang="it">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{titolo}</title>
    <meta name="description" content={descrizione} />
    <link rel="canonical" href={canonical} />
  </head>
  <body>
    <nav><a href="/">Home</a> · <a href="/guida/">Guida</a></nav>
    <main><slot /></main>
    <footer>© {new Date().getFullYear()}</footer>
  </body>
</html>

I layout si annidano liberamente: LayoutArticolo.astro importa Base.astro e gli passa le props. È il modo normale di avere “un layout per i post” e “un layout per le pagine” senza duplicare l’<head>.

Astro 7 è severo sull'HTML

Il compilatore è stato riscritto in Rust ed è l’unico disponibile. Non corregge più l’HTML malformato: un tag non chiuso è un errore di build, non un avvertimento. In pratica è un miglioramento, ma se migri un progetto vecchio preparati a qualche errore al primo build.

📖 Layouts

2.5 Stili

<style> dentro un componente .astro è scoped di default: Astro aggiunge un attributo data-astro-cid-* agli elementi e lo usa come selettore. Non serve fare nulla.

<h1>Titolo</h1>
<style>
  h1 { color: rebeccapurple; }  /* colpisce solo l'h1 di QUESTO componente */
</style>

Per uscire dallo scope:

<style is:global>
  body { margin: 0; }
</style>
 
<style>
  article :global(h2) { border-bottom: 1px solid; }  /* mirato */
</style>

:global() è quasi sempre la risposta giusta quando devi stilare HTML che non scrivi tu, per esempio il markup generato dal Markdown, che non ha l’attributo scope del componente. È l’errore numero uno di chi stila una guida: scrivi h2 { ... } nel componente che renderizza il contenuto, non funziona nulla, e non capisci perché. La risposta è :global(h2).

Precedenza (a parità di specificità, dal più debole al più forte): <link> nell’head → CSS importato → stili scoped. Gli scoped vincono sempre a parità di specificità.

class:list

Sostituisce clsx. Stringhe aggiunte, chiavi di oggetto se truthy, array appiattiti, false/null/undefined ignorati:

<div class:list={['card', { attiva: isAttiva, disabilitata: !abilitato }, extraClassi]}>

define:vars

Passa valori dal frontmatter al CSS come custom properties:

---
const accento = 'oklch(0.7 0.15 250)';
---
<style define:vars={{ accento }}>
  a { color: var(--accento); }
</style>

Su <script> no

define:vars su un <script> implica is:inline: lo script non viene più bundlato né deduplicato. La documentazione stessa consiglia di non usarlo per gli script e di passare i dati con attributi data-* (vedi 05 - Interattività - island, script e view transitions). Su <style> invece è perfetto.

Passare una classe a un componente figlio

class è una parola riservata in JS, quindi va rinominata nel destructuring:

---
const { class: classe, ...resto } = Astro.props;
---
<div class:list={['base', classe]} {...resto}><slot /></div>

Tailwind

Dal 2025 Tailwind 4 si installa come plugin Vite, non come integrazione Astro:

npx astro add tailwind

Questo aggiunge @tailwindcss/vite alla config e crea src/styles/global.css con @import "tailwindcss";, che poi importi nel layout. Niente più tailwind.config.cjs: la configurazione di Tailwind 4 è CSS-first.

@astrojs/tailwind è legacy

L’integrazione integrations: [tailwind()] che trovi in tutti i tutorial vecchi serve solo per Tailwind 3. Non usarla su progetti nuovi.

📖 Styling

2.6 Import e asset

Astro importa nativamente: .astro, .md/.mdx, .js/.ts, .json, .css, .module.css, immagini, pacchetti npm.

---
import dati from '../data/prodotti.json';
import '../styles/globale.css';
import immagine from '../assets/foto.jpg';   // oggetto con .src, .width, .height, .format
import testoGrezzo from '../snippets/esempio.py?raw';
---
<img src={immagine.src} width={immagine.width} height={immagine.height} alt="Foto" />
<pre>{testoGrezzo}</pre>

Il suffisso ?raw è oro per una guida tecnica: importi un file sorgente vero e lo mostri, invece di copiaincollarlo dentro il Markdown e vederlo divergere dal codice reale. Esistono anche ?url e ?inline.

Glob import: solo stringhe letterali, niente variabili:

const moduli = import.meta.glob('../snippets/*.ts', { eager: true, query: '?raw', import: 'default' });

Astro.glob() non esiste più

Rimosso in Astro 6. Usa import.meta.glob(). Per il contenuto strutturato, però, la risposta giusta sono le content collections, non il glob.

Alias in tsconfig.json, così eviti i ../../../:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@componenti/*": ["src/components/*"],
      "@layout/*": ["src/layouts/*"]
    }
  }
}

📖 Imports

2.7 Configurazione minima da conoscere

// astro.config.mjs
import { defineConfig } from 'astro/config';
 
export default defineConfig({
  site: 'https://esempio.it',      // serve a sitemap, RSS, canonical
  base: '/',                       // se il sito sta in una sottocartella
  trailingSlash: 'ignore',         // 'always' | 'never' | 'ignore'
  build: { format: 'directory' },  // 'directory' | 'file' | 'preserve'
  integrations: [],
  markdown: {},
});

site è quello che dimenticano tutti: senza, Astro.site è undefined, e sitemap, RSS e URL canonici non funzionano.

Esercizi

  1. Costruisci Base.astro + Articolo.astro (che usa Base) + tre pagine che li usano. Verifica che l’<head> sia scritto in un posto solo.
  2. Crea un componente Callout.astro con <slot /> e una prop tipo (info | attenzione | pericolo) che cambia i colori con class:list. Ti servirà nel modulo 4 per usarlo dentro MDX.
  3. Prendi un file .ts reale del tuo progetto, importalo con ?raw in una pagina e mostralo dentro un <pre>. Modifica il file e ricarica: il contenuto deve seguirlo.
  4. Scrivi uno stile per gli h2 del contenuto Markdown. Falo prima senza :global() per vedere il problema con i tuoi occhi, poi correggi.