Errori e fix
Next.js App Router: 404 dopo il deploy, senza errori di build
La build passa, la pagina non esiste in produzione. Le cause sono quasi sempre tre: file statici non copiati, prefisso di percorso diverso, rotta dinamica non generata.
In sintesi
- Con `output: "standalone"` devi copiare a mano `.next/static` e `public`: senza, la pagina c'è ma gli asset no.
- `basePath` e `trailingSlash` cambiano gli URL: verifica il percorso reale, non quello che vedi in locale.
- Le rotte dinamiche con `dynamicParams = false` restituiscono 404 per ogni parametro non generato.
Risposta breve: la pagina esiste nella build ma non nel deploy. Controlla tre cose in quest'ordine: file statici copiati, percorso reale con basePath e trailingSlash, rotta dinamica generata.
Le cause, in ordine di frequenza
| Causa | Come la riconosci | Correzione |
|---|---|---|
| `output: "standalone"` senza asset | HTML 200, CSS e JS 404 | copia `.next/static` e `public` |
| `basePath` diverso | tutti gli URL interni sbagliati | allinea link e configurazione |
| `trailingSlash` cambiato | redirect o 404 su URL con e senza slash | scegli una forma e mantienila |
| rotta dinamica non generata | 404 solo su alcuni slug | aggiungi `generateStaticParams` |
Il meccanismo: tre livelli che possono rispondere 404
Una richiesta attraversa tre strati e ognuno può chiuderla con lo stesso codice. Il primo è l'infrastruttura: reverse proxy, CDN o ingress che non conosce il percorso, o lo conosce con un prefisso diverso. Il secondo è il middleware di Next.js: una riscrittura o un matcher può dirottare la richiesta prima che il router dei segmenti la veda. Il terzo è il segmento: nessun page.tsx corrisponde, oppure il codice chiama notFound() perché il dato non esiste. I tre casi producono lo stesso corpo HTML e lo stesso status, ma i log parlano lingue diverse: il primo non lascia traccia nell'applicazione, il terzo sì.
Un passo diagnostico prima di ricostruire
Prima di cambiare configurazione, confronta l'elenco delle rotte generate con quello che il server risponde. next build scrive .next/prerender-manifest.json, che elenca le rotte prerenderizzate: se la pagina che risponde 404 non è lì, il problema è a monte del deploy. Se è lì e risponde comunque 404, il problema è il percorso con cui la chiami.
# 1. quante rotte ha davvero generato la build
node -e "const m=require('./.next/prerender-manifest.json');const r=Object.keys(m.routes||{});console.log(r.length,r.slice(0,3))"
# 2. quale percorso risponde davvero
curl -o /dev/null -s -w "%{http_code} %{redirect_url}\n" https://example.com/blog/mio-post
La correzione per standalone
# dopo next build
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
node .next/standalone/server.js
Sono due copie che Next.js non fa per te. Se usi un Dockerfile, le stesse righe vanno nel layer finale, altrimenti il container parte con l'HTML e senza gli asset.
La correzione per le rotte dinamiche
// app/blog/[slug]/page.tsx
export const dynamicParams = false;
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
Con dynamicParams = false, ogni slug non presente nell'elenco risponde 404: è il comportamento voluto, ma va ricordato quando si aggiunge un contenuto senza rigenerare la build.
Diagnosi in tre comandi
next build # elenca le rotte generate
curl -I https://example.com/la-pagina # stato reale
curl -I https://example.com/_next/static/... # asset
Se l'HTML risponde 200 e il CSS 404, il problema è la copia degli asset. Se l'HTML risponde 404, il problema è il routing o il percorso.
Quando la correzione non funziona
Il caso silenzioso è il case del file: su Windows About.tsx e about.tsx convivono, su Linux no. Un import con il case sbagliato passa in locale e fallisce in produzione. Un secondo caso è una regola di middleware che riscrive il percorso prima del routing: il 404 arriva dal middleware, non dalla pagina.
Quando la correzione comune peggiora le cose
La correzione che circola per ogni 404 è una rotta catch-all: app/[...slug]/page.tsx che restituisce una pagina «non trovata» con status 200. Il sintomo esce dai monitor, il problema resta. Google chiama queste pagine soft 404 e le tratta come errori; chi visita vede un contenuto che sembra esistere; il test che controllava lo status smette di fallire e la regressione diventa muta. Il catch-all serve per una pagina di ricerca, non per mascherare un routing rotto: se lo tieni, chiama notFound() al suo interno, così lo status resta 404.
Un limite con un nome: la finestra di staleness
Con generateStaticParams l'elenco delle pagine è fissato al momento della build. Un articolo pubblicato dopo il deploy non ha una rotta generata e, con dynamicParams = false, risponde 404 fino al deploy successivo. Non è un bug, è la definizione di quella modalità. Il limite si chiama staleness e si gestisce scegliendo: rigenerazione incrementale con revalidate, un webhook che ricostruisce, oppure dynamicParams = true accettando il costo di una pagina renderizzata a richiesta.
Come prevenirlo
- Nel Dockerfile, verifica che
publice.next/staticesistano nel layer finale. - Testa gli URL con e senza slash dopo ogni modifica alla configurazione.
- Aggiungi uno smoke test post-deploy su tre rotte: home, una statica, una dinamica.
Uno smoke test su tre rotte copre il caso generale, non la pagina appena aggiunta. Metti nel deploy un confronto tra le rotte del manifest e una lista di URL attesi, e fallisci se manca una voce: è l'unico controllo che vede un contenuto nuovo senza rotta prima che lo veda un utente.
Dove continuare
Nel catalogo: dashboard con shell, componenti e blocchi. Le guide: struttura App Router e peso di una libreria UI; il confronto: alternative a MUI.
Domande frequenti
Perché in locale funziona?
In locale Next.js serve `public` e `.next/static` dalla cartella di sviluppo. In produzione con standalone quei file non vengono inclusi automaticamente.
Come vedo le rotte generate?
L'output di `next build` elenca ogni rotta con tipo (statica, dinamica, SSG). Se la tua non c'è, il file non è dove Next.js lo cerca.
`page.tsx` nella cartella giusta non basta?
Serve anche che la cartella sia sotto `app`. Un file in `src/pages` convive con App Router ma segue un altro sistema di routing.
Perché 404 solo su alcune pagine?
Spesso sono le dinamiche senza `generateStaticParams`. Se `dynamicParams` è false, tutto ciò che non è elencato risponde 404.
Il problema può essere il case del file?
Sì. Su Linux il file system distingue maiuscole e minuscole; su Windows no. Un import con il case sbagliato passa in locale e fallisce in produzione.