06. Rendering on-demand: adapter, middleware, actions, sessioni
← 05 - Interattività - island, script e view transitions · Indice · successivo → 07 - Immagini, font, SEO, performance e deploy
Questo modulo serve quando il sito deve fare qualcosa a richiesta: leggere cookie, gestire un form, mostrare contenuti personalizzati, proteggere una pagina. Per una guida o un blog puramente statico puoi saltarlo e tornarci quando serve.
6.1 Adapter
Senza adapter, Astro produce solo file statici. L’adapter è il pezzo che sa come girare su una piattaforma specifica.
npx astro add node # server tuo, VPS, Docker
npx astro add netlify
npx astro add vercel
npx astro add cloudflareimport node from '@astrojs/node';
export default defineConfig({
adapter: node({ mode: 'standalone' }), // 'standalone' | 'middleware'
});Serve un adapter anche a un sito completamente statico se vuoi usare le
server island (server:defer) o l’image CDN di alcuni host.
Note pratiche per piattaforma:
- Node:
mode: 'standalone'genera un server avviabile connode ./dist/server/entry.mjs;mode: 'middleware'esporta un handler da montare dentro Express/Fastify. L’adapter non legge.enva runtime: le variabili le passi dall’ambiente. - Cloudflare: riscritto in Astro 6: solo Workers (il supporto a Pages è
stato rimosso), il dev server gira su workerd reale, e
Astro.locals.runtimenon esiste più (si usaimport { env } from 'cloudflare:workers'). - Netlify / Vercel: supportano cache CDN, skew protection, e ISR (Vercel).
6.2 Rendere on-demand una singola rotta
---
export const prerender = false;
const utente = Astro.cookies.get('sessione')?.value;
if (!utente) return Astro.redirect('/login');
---
<p>Ciao {utente}</p>Cosa si sblocca in on-demand: Astro.cookies, Astro.request completo (headers,
method, body), Astro.response (status e header), Astro.clientAddress,
Astro.session, gli endpoint dinamici, e lo streaming HTML.
Conseguenza dello streaming
Astro invia l’HTML man mano che lo produce. Quindi redirect e modifiche agli header vanno fatti a livello di pagina, non dentro un componente figlio o un layout: quando quel codice gira, gli header sono già partiti.
6.3 Middleware
Un file src/middleware.ts (oppure src/middleware/index.ts) che esporta
onRequest. Gira prima di ogni pagina, incluse quelle prerenderizzate durante la build.
// src/middleware.ts
import { defineMiddleware, sequence } from 'astro:middleware';
const autenticazione = defineMiddleware(async (context, next) => {
const token = context.cookies.get('token')?.value;
context.locals.utente = token ? await risolviUtente(token) : null;
if (context.url.pathname.startsWith('/area-riservata') && !context.locals.utente) {
return context.redirect('/login');
}
return next();
});
const intestazioni = defineMiddleware(async (_context, next) => {
const risposta = await next();
risposta.headers.set('X-Frame-Options', 'DENY');
return risposta;
});
export const onRequest = sequence(autenticazione, intestazioni);Tipizza locals in src/env.d.ts, altrimenti scrivi alla cieca:
declare namespace App {
interface Locals {
utente: { id: string; nome: string } | null;
}
}sequence() esegue i middleware in ordine all’andata e in ordine inverso al
ritorno: lo schema classico a cipolla.
6.4 Actions: form e RPC senza scrivere endpoint
Le Actions sono il modo idiomatico per gestire mutazioni (form, “mi piace”, iscrizione newsletter) con validazione e tipi condivisi tra server e client.
// src/actions/index.ts
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro/zod';
export const server = {
newsletter: defineAction({
accept: 'form',
input: z.object({
email: z.email(), // Zod 4: NON z.string().email()
condizioni: z.boolean(),
}),
handler: async ({ email, condizioni }, context) => {
if (!condizioni) {
throw new ActionError({ code: 'BAD_REQUEST', message: 'Devi accettare le condizioni.' });
}
await iscrivi(email);
return { ok: true };
},
}),
};---
// src/pages/iscriviti.astro
export const prerender = false;
import { actions, isInputError } from 'astro:actions';
const risultato = Astro.getActionResult(actions.newsletter);
if (risultato && !risultato.error) return Astro.redirect('/grazie');
const erroriCampo = isInputError(risultato?.error) ? risultato.error.fields : {};
---
<form method="POST" action={actions.newsletter}>
<input type="email" name="email" required />
{erroriCampo.email && <p class="errore">{erroriCampo.email.join(', ')}</p>}
<label><input type="checkbox" name="condizioni" /> Accetto</label>
<button>Iscrivimi</button>
</form>Dal client, come RPC tipizzata:
import { actions } from 'astro:actions';
const { data, error } = await actions.newsletter({ email, condizioni: true });Conversioni automatiche con accept: 'form': number → z.number(),
checkbox → z.coerce.boolean(), file → z.instanceof(File) (ricorda
enctype="multipart/form-data"), campi ripetuti → z.array(). I campi vuoti
diventano null.
Le action sono endpoint pubblici
Ogni action è raggiungibile a
/_actions/<nome>. L’autorizzazione va fatta dentro l’handler (o nel middleware), non nascondendo il form.
📖 Actions
6.5 Sessioni
Stabili dalla 5.7. Accesso con Astro.session (o context.session), metodi
asincroni: get, set, has, delete, destroy, regenerate.
---
export const prerender = false;
const carrello = (await Astro.session?.get('carrello')) ?? [];
await Astro.session?.set('carrello', [...carrello, prodotto]);
---// astro.config.mjs - forma corretta da Astro 6 in poi
import { defineConfig, sessionDrivers } from 'astro/config';
export default defineConfig({
adapter: vercel(),
session: {
driver: sessionDrivers.redis({ url: process.env.REDIS_URL }),
},
});Cambiato in Astro 6
Il driver non è più una stringa (
driver: 'redis') ma un oggetto costruito consessionDrivers.*. La forma vecchia (session.options) è deprecata. Alcune pagine di documentazione degli adapter mostrano ancora la sintassi vecchia: fidati della configuration reference.
Driver di default per adapter: Node → filesystem, Cloudflare → Workers KV,
Netlify → Netlify Blobs. Vercel e altri vanno configurati a mano. Le sessioni
non funzionano nell’edge middleware. Da Astro 7.2 puoi disattivarle del tutto
con session: false per alleggerire il bundle serverless.
📖 Sessions
6.6 Variabili d’ambiente: astro:env
Due sistemi convivono.
import.meta.env (di Vite): tutto ciò che ha prefisso PUBLIC_ finisce nel
bundle client, il resto resta server-side. Nessuna validazione, nessun tipo.
astro:env: schema dichiarato, validato e tipizzato.
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
PUBLIC_ANALYTICS_ID: envField.string({ context: 'client', access: 'public', optional: true }),
PORT: envField.number({ context: 'server', access: 'public', default: 4321 }),
API_SECRET: envField.string({ context: 'server', access: 'secret' }),
},
},
});import { PUBLIC_ANALYTICS_ID } from 'astro:env/client';
import { API_SECRET, getSecret } from 'astro:env/server';Le tre combinazioni valide sono client+public, server+public, server+secret. client+secret non esiste, per costruzione: se una cosa arriva al browser, non è un segreto.
Limite: astro:env è un modulo virtuale, quindi non funziona dentro
astro.config.mjs né in script esterni, lì usi process.env.
6.7 Server island
Il pezzo dinamico dentro una pagina statica. Componente .astro (non di framework)
con server:defer, e un adapter installato.
---
import Avatar from '../components/Avatar.astro';
---
<Avatar server:defer>
<AvatarGenerico slot="fallback" />
</Avatar>Come funziona: in build il componente viene estratto in una rotta dedicata; la
pagina viene servita statica con il fallback; il browser fa una GET verso
/_server-islands/<Nome> con le props cifrate nella query string, e sostituisce
il contenuto. Ogni isola carica indipendentemente.
Due dettagli che fanno perdere tempo se non li sai:
- Contesto isolato: dentro una server island,
Astro.urlvale/_server-islands/Avatar, non l’URL della pagina. Per risalire alla pagina reale leggi l’headerReferer. - Chiave di cifratura: viene generata a ogni build. Con deploy rolling o
multi-region serve una chiave stabile:
astro create-keye poi la variabile d’ambienteASTRO_KEY.
Props piccole: oltre ~2048 byte di URL, Astro passa a POST e la risposta non è più cacheabile.
6.8 Le novità di Astro 7 in quest’area
Route caching (stabile): cache dichiarativa per rotta:
import { defineConfig, memoryCache } from 'astro/config';
export default defineConfig({
cache: { provider: memoryCache() },
routeRules: {
'/api/[...path]': { swr: 600 },
'/prodotti/[...slug]': { maxAge: 3600, tags: ['prodotti'] },
},
});A runtime: Astro.cache.set({ maxAge: 120, tags: ['home'] }) e
Astro.cache.invalidate({ tags: ['prodotti'] }). Esistono provider CDN
(sperimentali) per Netlify, Vercel e Cloudflare.
Advanced routing (stabile): src/fetch.ts ti dà il controllo completo della
pipeline (trailing slash → redirect → sessioni → actions → middleware → pagine →
i18n → cache), con handler componibili da astro/fetch e un wrapper per Hono.
src/fetch.tsè ora un nome file riservato. Se ne avevi uno conquel nome per altri motivi, rinominalo o imposta
fetchFile: nullin config.
CSP (da Astro 6), security: { csp: true } inietta una Content Security
Policy con gli hash degli script e degli stili. Limiti dichiarati: non funziona in
dev (testa con build + preview), non è compatibile con <ClientRouter />
e non supporta Shiki (in quel caso serve <Prism />).
Esercizi
- Aggiungi l’adapter Node e trasforma una sola pagina in on-demand. Verifica con
astro buildche le altre restino file HTML indist/. - Scrivi un middleware che aggiunge un header di sicurezza e logga il tempo di risposta di ogni richiesta.
- Costruisci un form di contatto con un’Action: validazione, errori mostrati per campo, redirect dopo il successo.
- Prendi una pagina statica e mettici dentro una server island che mostra l’ora del server. Ricarica: la pagina è cacheabile, il pezzo no.