React
Middleware per il redirect di lingua
Un file middleware.ts legge la lingua del browser e manda la radice su /it o /en. Matcher, cookie, Vary e il loop che nasce senza la guardia.
In sintesi
- Il middleware decide l'URL prima della cache e prima della pagina.
- Il matcher deve escludere _next, le API e i file con estensione.
- Senza il controllo sulla lingua già presente il redirect entra in loop.
- Vary: Accept-Language evita che la CDN serva il redirect sbagliato.
Un redirect di lingua si fa in middleware.ts: leggi Accept-Language o un cookie e riscrivi l'URL verso /it o /en prima che la pagina renderizzi. Il middleware non cambia il contenuto, cambia l'indirizzo.
Il file minimo
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
const LOCALES = ["it", "en"] as const;
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const hasLocale = LOCALES.some(
(l) => pathname === `/${l}` || pathname.startsWith(`/${l}/`),
);
if (hasLocale) return NextResponse.next();
const cookie = request.cookies.get("locale")?.value;
const header = request.headers.get("accept-language") ?? "";
const preferred =
cookie && LOCALES.includes(cookie as "it" | "en")
? cookie
: header.toLowerCase().startsWith("it")
? "it"
: "en";
const url = request.nextUrl.clone();
url.pathname = `/${preferred}${pathname === "/" ? "" : pathname}`;
const response = NextResponse.redirect(url, 307);
response.headers.set("Vary", "Accept-Language, Cookie");
return response;
}
export const config = {
matcher: ["/((?!_next|api|.*\\..*).*)"],
};
Perché il matcher conta
Il middleware gira su ogni richiesta che matcha. Se il pattern include /_next o i file statici, ogni asset passa dal controllo e il redirect può colpire un'immagine. Il pattern qui sopra esclude _next, le API e tutto ciò che ha un punto nel nome.
Il loop e come evitarlo
Il loop nasce quando il redirect non controlla se la lingua è già nell'URL: /it finisce su /it/it, poi su /it/it/it. La guardia hasLocale è l'unica riga che impedisce il disastro.
| Segnale | Priorità | Perché |
|---|---|---|
Cookie locale | 1 | Scelta esplicita dell'utente, vince sul browser |
Accept-Language | 2 | Prima visita, nessuna scelta salvata |
| Fallback | 3 | Header assente o lingua non supportata |
Salva il cookie quando l'utente cambia lingua dal selettore, con path=/ e un max-age lungo. Così il redirect smette di ricalcolare dall'header a ogni visita.
Cache e runtime
- Il redirect è una risposta HTTP: senza
Varyuna CDN può servirla a tutti. - Il middleware gira sull'edge per default: niente driver di database incompatibili.
- Un rewrite interno con
NextResponse.rewritetiene l'URL pulito e serve/it/...sotto il cofano. - Il middleware non gira con
output: "export": un sito statico non ha un server che lo esegua.
Per il selettore servono dropdown o navigazione, montati in un blocco header.
Gli errori di hreflang sono in hreflang, errori comuni; il canonical in canonicalizzazione; un altro uso del middleware in middleware per l'autenticazione. Confronto: Remix vs Next.js.
curl -sI http://localhost:3000/
Limite noto: il middleware non vede il contenuto della pagina e non legge il database in modo portabile. Se la scelta dipende dall'account, decidi nella pagina, non nel middleware.
Domande frequenti
Dove va il file middleware?
Nella root del progetto o dentro src, allo stesso livello di app. Un solo file middleware.ts per progetto.
Posso usare il database nel middleware?
Solo con driver compatibili con l'edge runtime. Per query normali sposta la decisione in un server component o in un route handler.
Perché ottengo un loop di redirect?
Perché il middleware non controlla se l'URL ha già la lingua. Aggiungi la guardia sul primo segmento prima di redirigere.
Serve Vary: Accept-Language?
Sì. Senza quell'header una cache condivisa può restituire a tutti la lingua scelta dal primo visitatore.
Il redirect rovina la SEO?
No, se è 307 o 308 e punta a un URL con hreflang reciproco. Il problema è servire la stessa pagina su due URL senza canonical.