Next.js
Come creare un sito Next.js per la produzione: App Router, dati, cache e distribuzione
Crea un sito Next.js pronto per la produzione con App Router, Server Components, cache esplicita, modifiche sicure, SEO, test e distribuzione.
Un sito Next.js di produzione è più di una raccolta di componenti React. Instradamento, confini tra server e client, durata dei dati, metadati, stati di errore e contratto di distribuzione devono essere coerenti.
1. Decidi cosa deve fare il sito web
Parti dalle responsabilità del sito, non dalla libreria di componenti. Elenca route pubbliche e private, fonti dei contenuti, operazioni di modifica, requisiti di ricerca e servizi esterni. Questo indica quali pagine possono essere statiche, quali richiedono dati al momento della richiesta e dove deve avvenire l'autorizzazione.
In un tipico sito di prodotto, pagine di marketing e documentazione cambiano relativamente lentamente, il blog è generato dai contenuti e l'area account dipende dall'utente autenticato. Sono tre cicli di vita dei dati diversi. Trattarli tutti come un'applicazione a pagina singola renderizzata nel client elimina i vantaggi del rendering sul server; trattarli tutti come HTML statico rende impossibile l'area account.
Scrivi la mappa delle route prima dell'implementazione. Assegna a ogni pagina importante una funzione principale e un URL canonico. Decidi quali entità richiedono segmenti dinamici, come /blog/[slug], e quali parti dell'interfaccia devono persistere durante la navigazione in un layout condiviso.
- —Contenuti pubblici: home, prodotto, prezzi, chi siamo, blog, guide e pagine legali.
- —Contenuti applicativi: dashboard, impostazioni, fatturazione o altre route dipendenti dalla sessione.
- —Confini dei dati: file locali, CMS, database, API esterne e input degli utenti.
- —Esigenze operative: variabili di ambiente, verifiche dello stato, log, attività programmate e distribuzione.
2. Crea il progetto con impostazioni predefinite consapevoli
Questa guida usa l'App Router di Next.js 16, TypeScript e una directory src. Le attuali impostazioni predefinite di create-next-app sono ragionevoli per un nuovo progetto, ma flag espliciti rendono la configurazione riproducibile per il team e la CI.
Avvia il server di sviluppo, poi esegui subito una compilazione di produzione. Compilare il primo giorno individua problemi di versione Node.js, importazioni non supportate ed errori di configurazione prima che il progetto cresca attorno a essi. Mantieni il lockfile npm nel controllo di versione e usa npm ci nelle compilazioni automatizzate, così la risoluzione delle dipendenze resta riproducibile.
Terminale
npx create-next-app@latest northstar \
--ts --tailwind --eslint --app --src-dir \
--import-alias "@/*"
cd northstar
npm run dev
npm run build3. Organizza le route attorno ai percorsi degli utenti
L'App Router trasforma le cartelle in segmenti URL. Un file page.tsx rende pubblica una route; layout.tsx avvolge il segmento e i suoi discendenti; loading.tsx fornisce un fallback in streaming; error.tsx intercetta gli errori di rendering non gestiti; not-found.tsx gestisce le risorse mancanti. Colloca i file vicino alla route responsabile, anziché creare un'unica cartella globale di componenti senza confini chiari.
I gruppi di route come (marketing) e (app) organizzano le cartelle senza cambiare l'URL. Sono utili quando il sito pubblico e il prodotto autenticato richiedono layout diversi. I segmenti dinamici come [slug] ricevono parametri di route, mentre quelli catch-all come [...parts] acquisiscono più livelli.
Usa route parallele e intercettanti avanzate solo se l'interazione le richiede. Una pagina fotografica condivisibile che si apre come modale durante la navigazione nell'app è un buon caso d'uso. Una normale pagina di impostazioni non lo è. L'albero di route più semplice e coerente con il modello mentale dell'utente è di solito il più facile da analizzare durante il debug.
A practical App Router structure
src/app/
├── layout.tsx
├── globals.css
├── (marketing)/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── pricing/page.tsx
│ └── blog/
│ ├── page.tsx
│ └── [slug]/page.tsx
├── (app)/
│ ├── dashboard/page.tsx
│ ├── settings/page.tsx
│ └── loading.tsx
├── api/health/route.ts
├── not-found.tsx
└── global-error.tsx4. Mantieni il layout radice piccolo e stabile
Il layout radice è obbligatorio e gestisce gli elementi html e body. Inserisci lì ciò che è davvero globale: lingua del documento, variabili dei font per tutto il sito, CSS globale, un provider del tema se necessario e metadati predefiniti condivisi. Non usarlo come contenitore indistinto di query specifiche delle route o di un grande albero di provider lato client.
I layout persistono durante la navigazione nel client, quindi sono il posto giusto per navigazione stabile e struttura dell'interfaccia. Un file template.tsx si comporta diversamente: riceve una nuova chiave e viene rimontato quando cambia il suo segmento. Scegli un template solo se il ripristino durante la navigazione è intenzionale, per esempio per riavviare un'animazione d'ingresso o azzerare lo stato locale.
src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"], display: "swap" });
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: { default: "Northstar", template: "%s | Northstar" },
description: "Planning software for focused product teams.",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}5. Usa Server Components come impostazione predefinita
Pagine e layout sono Server Components, salvo che il modulo sia contrassegnato con la direttiva use client. Possono interrogare un database, leggere variabili di ambiente esclusive del server, chiamare servizi interni e inviare output renderizzato senza aggiungere il codice del componente al bundle del browser.
Aggiungi un Client Component nel perimetro interattivo minimo utile. Un filtro può richiedere stato, gestori di eventi e API degli URL; la griglia dei prodotti sottostante può rimanere un Server Component. Rendere l'intera pagina un Client Component trasferisce al browser più JavaScript e più responsabilità di recupero dati del necessario.
Le props che passano dal server al client devono essere serializzabili. Passa un modello di vista limitato, anziché un record del database con campi privati. Importa server-only nei moduli dei dati che non devono mai entrare nel bundle client: una violazione accidentale del confine diventa così un errore di compilazione.
A small client island inside a server-rendered page
// src/app/products/page.tsx — Server Component
import { getProducts } from "@/lib/data";
import ProductFilters from "./product-filters";
export default async function ProductsPage() {
const products = await getProducts();
return <ProductFilters products={products} />;
}
// src/app/products/product-filters.tsx — Client Component
"use client";
import { useState } from "react";
export default function ProductFilters({ products }) {
const [query, setQuery] = useState("");
const visible = products.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase()),
);
return <>{/* input and product list */}</>;
}6. Recupera i dati dove vengono renderizzati
Un Server Component asincrono può chiamare direttamente fetch, un ORM o un client del database. Così non serve creare un endpoint HTTP interno soltanto per permettere al codice renderizzato sul server di chiamare se stesso. Tieni la query in un modulo di accesso ai dati esclusivo del server quando è usata da più route o quando autorizzazione e definizione dell'output devono essere concentrate in un punto verificato.
Evita sequenze di richieste che si attendono a vicenda. Se due query sono indipendenti, avviale insieme con Promise.all. Se solo un componente figlio richiede dati più lenti, lascia che li recuperi da sé e racchiudilo in un confine Suspense. La pagina può così inviare HTML utile mentre la sezione più lenta termina.
Decidi quanto deve essere aggiornato ogni risultato prima di aggiungere una cache. I dati dell'account specifici dell'utente di solito vanno recuperati al momento della richiesta. Una tabella pubblica dei prezzi può essere memorizzata in cache. Un catalogo di prodotti può usare una cache con tag invalidata dopo che un editor pubblica una modifica.
Parallel server-side data fetching
import "server-only";
export default async function DashboardPage() {
const [account, activity] = await Promise.all([
getAccount(),
getRecentActivity(),
]);
return <Dashboard account={account} activity={activity} />;
}7. Tratta la cache come una decisione di prodotto
Next.js 16 rende Cache Components una funzione da attivare esplicitamente. Quando cacheComponents è abilitato, la direttiva use cache può memorizzare in cache una funzione asincrona o un componente. cacheLife ne definisce la durata, cacheTag assegna alle voci correlate una chiave comune di invalidazione e un confine Suspense invia in streaming il lavoro non memorizzato in cache, eseguito al momento della richiesta, accanto alla struttura in cache.
La cache non è automaticamente corretta solo perché il contenuto è pubblico. Chiediti cosa succede quando il risultato è obsoleto, come viene invalidato, se la cache è condivisa dalla piattaforma di distribuzione e se un valore della sessione può entrare nella chiave. Non salvare mai il risultato privato di un utente sotto una chiave che un altro utente può ricevere.
Usa updateTag da una Server Action quando lo stesso utente deve vedere subito una modifica. Usa revalidateTag con un profilo di cache appropriato quando il comportamento stale-while-revalidate è accettabile, oppure revalidatePath quando il confine di invalidazione corrisponde naturalmente a una pagina o a un layout. Mantieni l'invalidazione accanto alla modifica che rende obsoleti i dati in cache.
next.config.ts and src/lib/products.ts
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = { cacheComponents: true };
export default nextConfig;
// src/lib/products.ts
import "server-only";
import { cacheLife, cacheTag } from "next/cache";
export async function getProducts() {
"use cache";
cacheLife("hours");
cacheTag("products");
return db.product.findMany();
}8. Progetta gli stati di caricamento, vuoto, errore e contenuto assente
Il percorso senza errori è solo uno degli stati. loading.tsx crea un confine Suspense a livello di route e permette alla navigazione di mostrare subito un fallback. Aggiungi confini Suspense più piccoli quando sezioni indipendenti devono comparire separatamente. Uno skeleton utile conserva il layout finale, anziché sostituire la pagina con uno spinner che provoca un grande spostamento.
Gli errori previsti fanno parte del normale flusso di controllo. Un errore di validazione del modulo deve restituire un risultato tipizzato al modulo. Un record mancante deve chiamare notFound. Un errore di autorizzazione deve produrre la risposta o il reindirizzamento appropriato. Riserva error.tsx alle eccezioni non gestite, registra l'errore con contesto sufficiente sulla richiesta e sul rilascio per indagarlo e offri all'utente un'azione di recupero sicura.
I risultati vuoti non sono errori. La pagina di un nuovo account senza progetti deve spiegare cosa contiene e come creare il primo progetto. Questa piccola distinzione rende l'interfaccia più comprensibile e mantiene il monitoraggio concentrato sui guasti reali.
src/app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function PostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return <article>{/* post content */}</article>;
}9. Gestisci esplicitamente modifiche ed endpoint HTTP
Le Server Actions sono adatte alle modifiche avviate dall'interfaccia React. Contrassegna la funzione con use server, valida FormData sul server, verifica sessione e permessi dell'utente, esegui la scrittura e invalida la cache interessata. Tratta ogni Server Action esportata come un endpoint pubblico: nascondere il pulsante non è autorizzazione.
Usa Route Handlers per interfacce HTTP che vanno oltre un singolo modulo React, inclusi webhook, verifiche dello stato, risposte contenenti file ed endpoint chiamati da app mobili o terze parti. Esporta gestori per i verbi HTTP necessari e restituisci oggetti Web Response standard. Valida le firme prima di interpretare come attendibili i campi del webhook e rendi idempotenti le operazioni ripetute.
Tieni gli effetti collaterali fuori dal rendering. Inviare email, addebitare una carta o scrivere dati analitici nel corpo di un componente può avvenire più volte. Esegui queste azioni nel punto dedicato alle modifiche, registra stato sufficiente a rendere sicuri i nuovi tentativi e sposta il lavoro lungo in una coda quando la richiesta non deve aspettarlo.
src/app/projects/actions.ts
"use server";
import { updateTag } from "next/cache";
export async function createProject(formData: FormData) {
const session = await verifySession();
if (!session) throw new Error("Unauthorized");
const input = projectSchema.parse({
name: formData.get("name"),
});
await db.project.create({ data: { ...input, ownerId: session.userId } });
updateTag("projects");
}10. Integra i metadati di ricerca e condivisione in ogni route
L'ottimizzazione per la ricerca parte da una pagina utile e scansionabile e da un URL stabile. Assegna a ogni route indicizzabile un titolo specifico, una descrizione chiara, un H1 visibile, titoli di sezione significativi, link descrittivi e contenuti che rispondano pienamente alla query. I metadati non compensano una pagina povera di contenuti né più URL che pubblicano lo stesso contenuto.
Esporta un oggetto metadata statico quando i valori sono fissi. Usa generateMetadata quando titolo, descrizione, URL canonico o immagine dipendono dai dati della route. Imposta metadataBase una sola volta nel layout radice, così gli URL relativi delle pagine canoniche e delle immagini di condivisione si risolvono correttamente. Genera sitemap e file robots dalla stessa fonte che definisce le route pubbliche ed escludi dall'indicizzazione gli URL privati o duplicati.
Aggiungi Article, Product, BreadcrumbList o un altro tipo JSON-LD appropriato solo se la pagina visibile lo giustifica. I dati strutturati devono descrivere ciò che il lettore può vedere davvero. Validali dopo il rendering e aggiorna dateModified quando il contenuto cambia in modo sostanziale.
- —Usa app/robots.ts e app/sitemap.ts per generare i file dei crawler.
- —Aggiungi file opengraph-image e twitter-image quando una route richiede immagini di condivisione generate.
- —Reindirizza gli URL dismessi e scegli un unico host canonico, un protocollo e una politica sulla barra finale.
- —Collega le pagine correlate con testi dei link descrittivi, così utenti e crawler possono scoprirle.
Metadata for a dynamic article
import type { Metadata } from "next";
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: "Article not found" };
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: "/blog/" + post.slug },
openGraph: {
type: "article",
title: post.title,
description: post.excerpt,
images: [post.ogImage],
},
};
}11. Proteggi le prestazioni nella separazione tra componenti
Next.js offre suddivisione del codice per route, Server Components, prefetching, ottimizzazione delle immagini e strumenti per i font, ma il risultato dipende comunque dalle scelte applicative. Tieni sotto controllo il confine del client: una direttiva use client include quel modulo e le sue importazioni lato client nel grafo dei moduli del browser. Grandi librerie di grafici, editor, mappe e pacchetti di analisi richiedono strategie di caricamento consapevoli.
Usa next/image con dimensioni reali o un contenitore fill controllato, così il browser può riservare spazio. Usa next/font per ospitare direttamente e precaricare i file dei font necessari al design. Carica script di terze parti con next/script e con la strategia meno aggressiva che soddisfa i requisiti del business.
Misura il comportamento in produzione, non solo quello del server di sviluppo. Usa Lighthouse per verifiche di laboratorio, raccogli Core Web Vitals da visite reali, esamina query lente sul server e analizza il bundle client quando una route peggiora. I budget di prestazioni sono più utili quando specificano una route e una metrica, anziché un unico punteggio per tutto il sito.
A responsive image with reserved space
import Image from "next/image";
<Image
src="/product-dashboard.png"
alt="Northstar dashboard showing the weekly plan"
width={1600}
height={900}
sizes="(max-width: 768px) 100vw, 800px"
priority
/>12. Metti l'autenticazione accanto all'accesso ai dati
L'autenticazione dimostra l'identità; l'autorizzazione decide cosa può fare. Usa una libreria di autenticazione mantenuta, salvo che il prodotto abbia solide ragioni per gestire direttamente password, rotazione delle sessioni, recupero degli account e integrazione con i provider. Conserva i dati di sessione in cookie sicuri HTTP-only e mantieni le letture sensibili sul server.
Centralizza l'autorizzazione sicura nel livello di accesso ai dati e verificala di nuovo in ogni Server Action e Route Handler. Proxy può effettuare reindirizzamenti preliminari vicino al confine della route, ma non è l'unica protezione: gli utenti possono chiamare direttamente gli endpoint pubblici di modifica e il codice server annidato richiede controlli propri.
Solo le variabili di ambiente con prefisso NEXT_PUBLIC_ appartengono al codice del browser. Considera pubblici quei valori al momento della compilazione. Contrassegna i moduli dei dati privati con server-only, restituisci DTO limitati ai Client Components, valida ogni input e applica escaping o sanitizzazione ai contenuti formattati non attendibili prima di renderizzarli.
- —Usa cookie sicuri, HTTP-only e same-site per le sessioni quando il modello di autenticazione lo consente.
- —Verifica appartenenza o ruolo nel punto di ogni lettura e scrittura sensibile.
- —Verifica le firme dei webhook sul corpo originale della richiesta quando il provider lo richiede.
- —Aggiungi una Content Security Policy coerente con gli script e le risorse che il sito carica davvero.
- —Non passare mai segreti, record privati del database o dettagli di errore senza restrizioni nelle props dei Client Components.
13. Verifica il comportamento al livello giusto
Verifica con test unitari le regole di business pure senza coinvolgere Next.js. Usa test di integrazione per il livello dei dati, le Server Actions e i Route Handlers su confini realistici. Usa test nel browser per i pochi percorsi il cui fallimento bloccherebbe l'utente: accesso, creazione della risorsa principale, completamento del checkout o pubblicazione di contenuti.
L'accessibilità fa parte dell'implementazione e della revisione, non di un'unica scansione finale. Usa elementi semantici, focus da tastiera visibile, etichette associate ai campi dei moduli, messaggi di errore utili, contrasto sufficiente e rispetto delle preferenze di movimento ridotto. I controlli automatizzati rilevano solo una parte dei problemi; le verifiche con tastiera e lettore di schermo fanno emergere ciò che il solo albero dei componenti non mostra.
Integra la compilazione di produzione nell'integrazione continua. In Next.js 16, next build non esegue più il linter: esegui quindi lint, controllo dei tipi, test e compilazione di produzione come comandi espliciti. Avvia l'app compilata e verifica con smoke test stato e route critiche prima della promozione.
A straightforward CI verification sequence
npm ci
npm run lint
npx tsc --noEmit
npm test
npm run build
npm start14. Distribuisci il processo che hai testato
Un'app Next.js renderizzata sul server richiede un ambiente di esecuzione Node.js compatibile, l'output della compilazione, valori di ambiente, un comando di avvio e una verifica dello stato. Compila da un checkout pulito con il lockfile salvato nei commit. Conserva i valori privati nell'archivio dei segreti della piattaforma di distribuzione, fuori dal repository e dall'immagine.
Una route di verifica dello stato deve indicare se questo rilascio può ricevere traffico. Mantienila leggera ed evita di restituire segreti o dettagli interni. Esamina separatamente i log della compilazione e dell'ambiente di esecuzione, verifica la route generata e collega poi dominio personalizzato e TLS gestito. Se l'app dipende da disco locale, job in background, ottimizzazione delle immagini o Cache Components, conferma che l'ambiente di hosting supporti il comportamento scelto.
Adios esegue il normale server di produzione Next.js come processo Node.js persistente. Il manifest di distribuzione mantiene il contratto di compilazione, avvio, porta, ambiente di esecuzione e controlli di stato accanto al sorgente, così le stesse condizioni possono essere esaminate prima del rilascio.
adios.yaml
name: northstar
region: de
replicas: 1
build_cmd: npm ci && npm run build
start_cmd: npm start
runtime:
name: node@24
port: 3000
health_path: /api/health
memory_mb: 1024
env:
DATABASE_URL: secret://DATABASE_URL
AUTH_SECRET: secret://AUTH_SECRET15. Usa una lista di controllo per il rilascio
La revisione finale deve collegare il comportamento del prodotto a quello dell'ambiente di esecuzione. Verifica una compilazione pulita, una visita diretta a ogni route critica, la navigazione nel client tra layout, una dipendenza lenta, un errore di validazione previsto, un errore non gestito, un record mancante e una richiesta non autorizzata. Esamina il sorgente HTML delle pagine pubbliche per confermare che contenuti importanti e metadati siano presenti senza aspettare il JavaScript del client.
Verifica poi il recupero. Interrompi una dipendenza necessaria, invia due volte la stessa modifica, ruota un segreto e distribuisci una versione che fallisce la verifica dello stato. Un sito è pronto quando il team sa spiegare come si avvia, come può fallire, come vengono protetti gli utenti durante il guasto e come l'ultimo rilascio sano rimane disponibile.
- —Route: URL canonici, reindirizzamenti, comportamento 404, sitemap e regole robots sono corretti.
- —Rendering: i confini tra server e client sono intenzionali e le sezioni lente mostrano fallback utili in streaming.
- —Dati: cache, invalidazione, autorizzazione, stati vuoti e stati di errore sono coerenti con il prodotto.
- —Qualità: superamento di lint, controllo dei tipi, test, verifiche di accessibilità, compilazione di produzione e smoke test.
- —Operazioni: segreti, controlli di stato, log, dominio, TLS, rollback e responsabilità sono documentati.