Codegea

Desarrollo

Desarrollo de software a medidaSistemas, plataformas e integraciones hechas a la medida de tu operaciónSitios corporativosProfesional, rápido y pensado para generar confianzaTiendas onlineCatálogo, pagos y envíos listos para vender las 24 horasWooCommerceTiendas WooCommerce rápidas, con pagos y factura de cada paísLanding pagesUna página enfocada en un solo objetivo: convertir visitantes en clientesApps móvilesApps iOS y Android a medida que ponen tu negocio en el bolsillo de tus clientes

Integración y CMS

Integraciones CMSWordPress, Strapi, Directus y Arc XP conectados a todo tu ecosistemaArc XPSoporte, implementación e integraciones para la plataforma de mediosMigracionesMigra tu sitio, plataforma o base de datos sin perder nada

Crecimiento e infraestructura

Automatización con IALa tecnología hace lo repetitivo y tu equipo se enfoca en lo que importaSEO on-pageQue Google te encuentre y te recomiende: optimización técnica y de contenidoServidores y hostingServidores rápidos y seguros, monitoreados 24/7
RetainerTu equipo dev, por mesDesde 20h al mes. Cuota fija.
Un cotizador con IA para cotizar servicios en minutosCotizaciones profesionales en minutos, con seguimiento de proyectos, vendedores y pagos en un solo lugar.Ver el caso

Por tipo

EcommerceTiendas y catálogosSitios corporativosMarca y captaciónPlataformas y appsSoftware a medida

Explorar

Todos los proyectosCasos y resultadosClientesQuién confía en nosotrosRetainerEquipo dev mensual

Blog

Todos los artículosGuías y opiniónBoletínUn correo al mesContactoRespondemos en 24h

Últimos artículos

Así está construido codegea.com: Next.js, Payload, Turso y R2CMS · 2026-10-20Cloudflare Workers: qué son y cuándo conviene usarlosNode.js · 2026-10-19Live Preview en Payload con Next.jsCMS · 2026-10-19
BoletínÚnete a nuestro boletínNotas prácticas de web, SEO e IA.
ServiciosDesarrollo de software a medidaSitios corporativosTiendas onlineWooCommerceLanding pagesApps móvilesIntegraciones CMSArc XPMigracionesAutomatización con IASEO on-pageServidores y hostingTu equipo dev, por mes
ProyectosCotizaciones profesionales en minutos, con seguimiento de proyectos, vendedores y pagos en un solo lugar.EcommerceSitios corporativosPlataformas y appsTodos los proyectosClientesRetainer
Retainer
BlogTodos los artículosBoletínContactoAsí está construido codegea.com: Next.js, Payload, Turso y R2Cloudflare Workers: qué son y cuándo conviene usarlosLive Preview en Payload con Next.jsÚnete a nuestro boletín
ESEN
Hablemos

Web

Rutas traducidas en Next.js 16 sin librerías

Cómo tener /servicios en español y /services en inglés en Next.js 16 con un mapa de rutas y un proxy.ts, sin añadir librerías de internacionalización.

Derian Pinto · Founder & Full Stack Developer2026-10-183 min

Cuando un sitio tiene dos idiomas, lo habitual es traducir los textos y dejar las URLs en un solo idioma: /es/services y /en/services. Para SEO es mejor que la URL también esté traducida: /servicios en español y /en/services en inglés. Next.js no lo hace solo, y no hace falta una librería para conseguirlo. Así lo resolvimos en este sitio.

La idea

Las carpetas de app/ tienen un nombre interno fijo, en inglés: app/[locale]/services, app/[locale]/contact. Lo que ve el visitante es otra cosa. Un archivo proxy.ts traduce la URL pública a la carpeta interna antes de que Next.js renderice nada.

  • El español es la raíz, sin prefijo: /servicios, /contacto.
  • El inglés va bajo /en: /en/services, /en/contact.
  • La carpeta es la misma en ambos casos.

Desde Next.js 16 este archivo se llama proxy.ts (antes middleware.ts; la convención antigua está marcada como obsoleta en la documentación).

Paso 1: un único mapa de rutas

Toda la información vive en un solo archivo, i18n/routes.ts:

export const SEGMENTS = {
  services: { internal: 'services', es: 'servicios', en: 'services' },
  contact: { internal: 'contact', es: 'contacto', en: 'contact' },
  privacy: {
    internal: 'privacy',
    es: 'politica-de-privacidad',
    en: 'privacy-policy',
  },
  // …
} as const;

Cada entrada dice cómo se llama la carpeta interna y cómo se escribe en cada idioma. Ningún componente escribe una URL a mano: todos usan funciones como segmentPath('en', 'contact'), que devuelve /en/contact. Si un día cambias contacto por escribenos, lo cambias en un sitio.

Paso 2: construir las URLs con una función

const prefix = (locale: Locale) =>
  locale === DEFAULT_LOCALE ? '' : `/${locale}`;

export const segmentPath = (locale: Locale, key: SegmentKey) =>
  `${prefix(locale)}/${SEGMENTS[key][locale]}`;

El idioma por defecto no lleva prefijo; los demás sí.

Paso 3: el proxy traduce

proxy.ts mira la URL, detecta el idioma y reescribe hacia la carpeta interna:

export function proxy(req: NextRequest) {
  const { pathname, search } = req.nextUrl;
  const { locale, explicit, rest } = splitLocale(pathname);
  const prefix = locale === DEFAULT_LOCALE ? '' : `/${locale}`;

  // /es/... se redirige a la URL sin prefijo: una sola URL por página
  if (explicit && locale === DEFAULT_LOCALE) {
    return NextResponse.redirect(new URL(join(...rest) + search, req.url), 308);
  }

  let internal = rest;
  const match = rest[0] ? matchSegment(locale, rest[0]) : null;
  if (match) {
    // pidieron /services en español: redirige a la forma canónica /servicios
    if (match.canonical) {
      return NextResponse.redirect(
        new URL(prefix + join(match.canonical, ...rest.slice(1)) + search, req.url),
        308,
      );
    }
    internal = [match.internal, ...rest.slice(1)];
  }

  return NextResponse.rewrite(new URL(join(locale, ...internal) + search, req.url));
}

Hace tres cosas:

  1. Reescribe /servicios a /es/services. El visitante sigue viendo /servicios.
  2. Redirige (308) a la forma correcta cuando alguien entra con el segmento de otro idioma, por ejemplo /services en español. Así cada contenido tiene una sola URL.
  3. Quita el prefijo /es del idioma por defecto con otra redirección.

Y el matcher deja fuera lo que no debe pasar por aquí:

export const config = {
  matcher: ['/((?!api/|admin(?:/|$)|_next/).*)'],
};

Paso 4: dile a Google que son la misma página

Con URLs distintas por idioma, necesitas hreflang. Cada página debe saber cuál es su equivalente en el otro idioma. Nosotros pasamos las URLs alternas a un único componente de plantilla, que genera el selector ES | EN y las etiquetas hreflang. Un artículo sin traducción simplemente no se anuncia en el otro idioma.

Lo que gana y lo que cuesta

Gana: URLs legibles en cada idioma, ninguna dependencia, y todo el comportamiento en un archivo que puedes leer de principio a fin.

Cuesta: tienes que mantener tú el mapa y las redirecciones. Si el sitio crece a muchos idiomas, o necesitas formatear fechas, plurales o monedas por idioma, una librería de i18n sigue siendo una buena idea. Para dos idiomas y un sitio de contenido, el mapa se queda corto antes de ser un problema.

Dos advertencias

  • Prueba las redirecciones con curl -I. Un bucle de redirecciones entre /es/x y /x se ve rápido, pero solo si lo pruebas.
  • No pongas en el matcher rutas que no necesitan traducción, como /api o el panel de administración. En nuestro caso, el admin de Payload queda fuera.

Si tu sitio es en Next.js y quieres ver cómo cambió el framework, lee qué cambia en Next.js 16 para sitios corporativos.

¿Compartir este artículo?LinkedInWhatsAppX

Escrito por

Derian Pinto

Founder & Full Stack Developer

Fundador de Codegea. Desarrolla sitios, tiendas y plataformas a medida, e integra y da soporte a Arc XP para medios.

Únete a nuestro boletín

Un correo con novedades y notas prácticas de web, SEO e IA. Sin spam, y te das de baja cuando quieras.

¿Montamos tu proyecto?

¿Necesitas ayuda? Chatea con nosotros