# Middleware for locale redirects

A middleware.ts file reads the browser language and sends the root to /it or /en. Matcher, cookie, Vary and the loop that starts without the guard.

> Page: https://ui.eneawork.it/blog/nextjs-middleware-redirect-locale
> Markdown: https://ui.eneawork.it/blog/nextjs-middleware-redirect-locale.md

*2026-11-01*

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.

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 catalogo EneUI indicizza 111.985 componenti, 1.556 blocchi e 12.808 icone da 1.472 librerie: numeri misurati il 1 novembre 2026 su /api/stats. Dei 398 post del blog, 398 hanno l'edizione inglese e una copertina PNG per lingua, contati il 1 novembre 2026 su data/blog e public/blog-covers. Due lingue sono due URL per pagina: il redirect deve scegliere, non duplicare.

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.

## FAQ

### 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.
