Barra final solo en la raíz de idioma (/es/), resto de rutas sin barra

trailingSlash true ponía barra en todo (/es/content-creators/). Se pasa a
skipTrailingSlashRedirect + normalización en middleware: raíz de idioma /es → /es/
(con barra), y cualquier sub-ruta con barra final se redirige sin ella
(/es/x/ → /es/x). Sin bucles. Las /api quedan fuera del middleware (sin 308).
El redirect del logout apunta a /es/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-14 15:36:13 +00:00
parent d0490e06fe
commit 434011624c
6 changed files with 33 additions and 6 deletions
+1
View File
@@ -18,5 +18,6 @@ export async function GET(_request: Request, { params }: { params: Promise<{ loc
const session = await getSession()
session.destroy()
// La raíz de idioma es canónica CON barra (/es/); el resto de rutas sin barra.
return new NextResponse(null, { status: 303, headers: { Location: `/${loc}/` } })
}
+1 -1
View File
@@ -38,7 +38,7 @@ export function LoginForm() {
// Navegación completa (no del lado cliente): fuerza que el servidor
// re-renderice el layout/cabecera con la sesión nueva. Con router.push
// el layout persiste y la cabecera seguía mostrando "Conectar".
window.location.assign(`/${locale}${data.needsSelection ? '/select-account/' : '/my-account/'}`)
window.location.assign(`/${locale}${data.needsSelection ? '/select-account' : '/my-account'}`)
return
} else {
const key = (ERROR_KEYS as readonly string[]).includes(data.error ?? '') ? data.error! : 'genericError'
@@ -28,7 +28,7 @@ export function SelectAccountForm({ accounts }: { accounts: GameAccount[] }) {
})
const data: { success?: boolean } = await res.json()
// Navegación completa: re-renderiza el layout/cabecera con la cuenta ya elegida.
if (data.success) window.location.assign(`/${locale}/my-account/`)
if (data.success) window.location.assign(`/${locale}/my-account`)
else {
setError(t('error'))
setBusy(null)
+1 -1
View File
@@ -118,7 +118,7 @@ export function SiteHeader({
<Link href="/my-account">MI CUENTA</Link>
</p>
<p>
<a href={`/${locale}/log-out/`}>DESCONECTAR</a>
<a href={`/${locale}/log-out`}>DESCONECTAR</a>
</p>
</div>
</div>
+25 -1
View File
@@ -1,7 +1,31 @@
import createMiddleware from 'next-intl/middleware'
import { NextResponse, type NextRequest } from 'next/server'
import { routing } from './i18n/routing'
export default createMiddleware(routing)
const intlMiddleware = createMiddleware(routing)
// Raíces de idioma: «/es», «/en». Estas SÍ llevan barra final; el resto NO.
const LOCALE_ROOTS = new Set(routing.locales.map((l) => `/${l}`))
export default function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
// 1) Raíz de idioma sin barra → con barra: /es → /es/
if (LOCALE_ROOTS.has(pathname)) {
const url = new URL(request.url)
url.pathname = `${pathname}/`
return NextResponse.redirect(url)
}
// 2) Sub-ruta con barra final → sin barra (la raíz /es/ se conserva): /es/x/ → /es/x
if (pathname.length > 1 && pathname.endsWith('/') && !LOCALE_ROOTS.has(pathname.slice(0, -1))) {
const url = new URL(request.url)
url.pathname = pathname.replace(/\/+$/, '')
return NextResponse.redirect(url)
}
return intlMiddleware(request)
}
export const config = {
// Aplica a todo salvo /api, estáticos de Next e ipas con extensión.
+4 -2
View File
@@ -4,8 +4,10 @@ import createNextIntlPlugin from 'next-intl/plugin'
const withNextIntl = createNextIntlPlugin('./i18n/request.ts')
const nextConfig: NextConfig = {
// URLs con barra final (/es/ en vez de /es), como el sitio original.
trailingSlash: true,
// Barra final SOLO en la raíz de idioma (/es/, /en/); el resto de rutas SIN
// barra (/es/content-creators). Desactivamos el auto-redirect de Next: la
// normalización la hace el middleware con lógica estricta de pathname.
skipTrailingSlashRedirect: true,
}
export default withNextIntl(nextConfig)