Un patio de cambio de vías férreas con rieles de acero que se bifurcan bajo la luz de la mañana, como tráfico redirigido

Gestionar redirecciones a gran escala: arquitectura para migraciones

•13 min de lectura

Gestionar redirecciones a gran escala es la parte de una migración web que suele quedar en manos de quien tenga tiempo, y luego falla en silencio: un archivo de configuración supera el límite de la plataforma, un editor agrega una regla que crea un bucle o aparece una cadena de tres saltos después del segundo rediseño. Este artículo trata las redirecciones como un pequeño sistema de producción con su propio modelo de datos, validación, ejecución, modos de falla y pruebas. Si necesitas el lado del proceso SEO (inventario de URLs, mapeo, revisiones el día del lanzamiento), empieza por nuestra checklist de migración de sitio web para conservar tu SEO; aquí nos enfocamos en la ingeniería que hay detrás.

Por qué una lista en un archivo de configuración deja de funcionar

La mayoría de los frameworks permiten declarar redirecciones en la configuración, y para unas cuantas decenas de reglas es la respuesta correcta. Los problemas aparecen cuando la lista crece.

La solución no es un archivo más grande. Es separar dónde se almacenan y validan las redirecciones de dónde se resuelven.

Modelo de datos: las redirecciones son datos, no código

Guarda las redirecciones en una tabla con suficientes metadatos para responder "¿por qué existe esto y todavía se usa?". Un esquema inicial funcional:

CREATE TABLE redirects (
  id             BIGSERIAL PRIMARY KEY,
  source_path    TEXT NOT NULL,          -- normalizada, p. ej. /blog/old-post
  match_type     TEXT NOT NULL DEFAULT 'exact'
                 CHECK (match_type IN ('exact', 'prefix', 'pattern')),
  destination    TEXT NOT NULL,          -- ruta o URL absoluta en un host permitido
  status_code    SMALLINT NOT NULL DEFAULT 301
                 CHECK (status_code IN (301, 302, 307, 308, 410)),
  preserve_query BOOLEAN NOT NULL DEFAULT TRUE,
  origin         TEXT NOT NULL,          -- 'migration-2025', 'editor', 'import'
  created_by     TEXT NOT NULL,
  created_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
  hit_count      BIGINT NOT NULL DEFAULT 0,
  last_hit_at    TIMESTAMPTZ,
  UNIQUE (source_path, match_type)
);

Notas de diseño:

  • match_type separa las reglas exactas de los patrones. Las reglas exactas son una búsqueda en un hash. Las reglas de prefijo y regex son un problema distinto y más lento, y conviene saber cuántas tienes.
  • El 410 vive en la misma tabla. Algunas URLs antiguas no tienen equivalente; registrar esa decisión de forma explícita es mejor que dejarlas caer en un 404 genérico.
  • origin y created_by hacen posibles las auditorías. Cuando un lote de migración resulta incorrecto, puedes revertir ese lote sin tocar las reglas de los editores.
  • hit_count y last_hit_at te dicen qué reglas siguen recibiendo tráfico. Actualízalos de forma asíncrona a partir de los logs, nunca dentro de la petición.

Normaliza una vez, al escribir

La mayoría de los errores de redirección son errores de coincidencia: /Blog/Post/, /blog/post y /blog/post?utm_source=x son cadenas distintas. Elige una forma canónica y aplica la misma función al escribir reglas y al buscarlas.

export function normalizePath(input: string): string {
  const url = new URL(input, 'https://placeholder.local');
  let path = decodeURIComponent(url.pathname);
  path = path.replace(/\/{2,}/g, '/');          // colapsa dobles diagonales
  if (path.length > 1 && path.endsWith('/')) {
    path = path.slice(0, -1);                    // quita la diagonal final
  }
  return path.toLowerCase();                     // solo si el sitio anterior no distinguía mayúsculas
}

Dos decisiones aquí merecen elegirse a conciencia y no por defecto. Pasar a minúsculas es seguro solo si la plataforma anterior trataba las rutas sin distinguir mayúsculas; de lo contrario puedes fusionar URLs que antes eran distintas. Los query strings se excluyen de la llave a propósito: se compara la ruta y luego se pasa el query string al destino cuando preserve_query es verdadero, para que los parámetros de seguimiento sobrevivan.

Valida el grafo antes de publicar

Un conjunto de redirecciones es un grafo dirigido. Trata cada escritura (una regla o una importación masiva) como una operación sobre el grafo y rechaza cualquier cosa que lo deje en mal estado.

type Rule = { source: string; destination: string; status: number };

export function flatten(rules: Map<string, Rule>, maxHops = 5) {
  const errors: string[] = [];
  for (const [source, rule] of rules) {
    const seen = new Set([source]);
    let target = rule.destination;
    let hops = 0;
    while (rules.has(target)) {
      if (seen.has(target)) { errors.push(`loop: ${[...seen].join(' -> ')}`); break; }
      if (++hops > maxHops) { errors.push(`chain too long from ${source}`); break; }
      seen.add(target);
      target = rules.get(target)!.destination;
    }
    rule.destination = target;   // cada regla apunta ahora a su destino final
  }
  return errors;
}

Además de detectar bucles y cadenas, ejecuta estas revisiones en CI o en la API de administración:

  1. El destino existe. Pide cada destino único y exige un 200 del sitio nuevo. Una redirección a un 404 es peor que no tener redirección, porque esconde el problema.
  2. El origen no es una página activa. Si una ruta de origen también existe como ruta real, una de las dos está mal.
  3. Sin autorredirecciones después de normalizar (/about/ a /about).
  4. Solo hosts permitidos para destinos absolutos (más sobre esto en la sección de seguridad).

Aplanar al escribir significa que en ejecución nunca se siguen cadenas: cada petición recibe un solo salto a la URL final.

Dónde resolver las redirecciones: los trade-offs

Hay cuatro lugares comunes para hacer la búsqueda, y los sitios grandes suelen combinarlos.

  • CDN o reverse proxy (reglas en el edge, archivos map de Nginx). Lo más rápido, y la petición nunca llega a tu aplicación. El costo es un pipeline de deploy separado y, en CDNs administrados, límites que dependen del plan y que debes revisar en la documentación de tu proveedor antes de comprometerte.
  • Configuración del framework (redirects() en Next.js, o sus equivalentes). Cero código en ejecución y se evalúa antes de que la app renderice. Bueno para un conjunto pequeño y estable de reglas exactas; limitado por los topes de rutas de la plataforma y por la evaluación en build.
  • Middleware con un almacén clave-valor. Las reglas se leen en cada petición, así que los cambios de los editores aplican sin deploy. La documentación de Next.js sugiere leer de un almacén como Edge Config de Vercel o Redis. El costo es una búsqueda por petición, salvo que la evites antes.
  • Respaldo en la aplicación. Revisar la tabla de redirecciones solo cuando la app está por devolver un 404. Barato, porque solo corre en fallos, pero más lento para los usuarios redirigidos y fácil de saltarse por accidente con rutas catch-all.

Un patrón que funciona bien: reglas exactas de migración en el edge o en un mapa compilado, reglas administradas por editores en middleware con un almacén clave-valor, y el manejador de 404 como última búsqueda que además registra los fallos.

Abaratar las búsquedas en middleware con un filtro de Bloom

El costo de las búsquedas en middleware es que corren en cada petición, incluida la gran mayoría que no son redirecciones. La guía de Next.js para gestionar redirecciones a gran escala recomienda usar un filtro de Bloom para verificar si una redirección podría existir antes de leer el archivo o la base de datos completa, y evitar importar un archivo grande de redirecciones en el middleware porque ralentiza cada petición.

Un filtro de Bloom puede dar falsos positivos pero nunca falsos negativos, así que un "no" te permite saltarte la búsqueda por completo y un "tal vez" dispara la búsqueda real:

// middleware.ts (proxy.ts en versiones recientes de Next.js)
import { NextResponse, type NextRequest } from 'next/server';
import { ScalableBloomFilter } from 'bloom-filters';
import filterJson from './redirects/bloom-filter.json';
import { normalizePath } from './lib/normalize';

const filter = ScalableBloomFilter.fromJSON(filterJson as any);

export async function middleware(req: NextRequest) {
  const key = normalizePath(req.nextUrl.pathname);
  if (!filter.has(key)) return NextResponse.next();       // seguro que no existe

  const res = await fetch(
    new URL(`/api/redirects?pathname=${encodeURIComponent(key)}`, req.nextUrl.origin),
    { signal: AbortSignal.timeout(300) }
  ).catch(() => null);

  if (!res || !res.ok) return NextResponse.next();        // falso positivo o error
  const { destination, status } = await res.json();
  const target = new URL(destination, req.nextUrl.origin);
  target.search = req.nextUrl.search;                     // conserva el query string
  return NextResponse.redirect(target, status);
}

El archivo del filtro se genera en el build a partir de la tabla de redirecciones. El route handler debe manejar los falsos positivos, y la misma documentación indica que debes validar las peticiones que recibe para evitar usos maliciosos. Ten en cuenta que en la documentación actual de Next.js el middleware se llama Proxy (proxy.ts).

Los filtros de Bloom solo ayudan con coincidencias exactas. Una discusión en GitHub de Next.js sobre más de 1,000 redirecciones señala que no sirven para comodines ni regex, y sugiere un matcher precompilado e indexado por prefijo para que cada petición pruebe solo un subconjunto pequeño de reglas con patrón. En la práctica: indexa las reglas con patrón por su primer segmento estático (/blog, /shop) y compila las regex una sola vez al iniciar.

Manejo de fallas

Decide de antemano qué pasa cuando el almacén de redirecciones está lento o caído.

  • Falla abierta. Si la búsqueda excede el tiempo límite, deja pasar la petición. Los usuarios verán un 404 en URLs antiguas por un rato, lo cual es recuperable; un 500 en cada petición no lo es.
  • Timeouts cortos y caché local. Guarda en memoria, por instancia y por poco tiempo, tanto aciertos como fallos; los fallos son la mayor parte del tráfico.
  • Snapshot de respaldo. Incluye en el artefacto de deploy el último JSON exportado de reglas exactas, para que el edge o la app puedan resolver el conjunto crítico de la migración aunque el almacén en vivo no esté disponible.
  • Importaciones masivas idempotentes. Las importaciones deben hacer upsert sobre (source_path, match_type) dentro de una transacción, para que repetir una importación fallida nunca cree duplicados.

Seguridad: no construyas un open redirect

Un servicio de redirecciones que acepta destinos arbitrarios es un open redirect esperando a ser usado para phishing.

  • Guarda los destinos como rutas siempre que puedas. Para URLs absolutas, verifica el host contra una lista permitida al escribir y otra vez al leer.
  • Nunca construyas el destino a partir de datos de la petición (parámetros, headers). La única entrada es la ruta normalizada que se usa como llave de búsqueda.
  • Protege la API de administración y el importador masivo con autenticación y un log de auditoría; para eso existe created_by en el esquema.
  • Limita la tasa y valida el endpoint interno de búsqueda, porque es accesible desde internet.

Observabilidad y pruebas

Las redirecciones fallan en silencio, así que hazlas visibles:

  • Registra cada redirección servida (origen, destino, estado) y agrégala en hit_count y last_hit_at con un job en segundo plano.
  • Registra los 404 con su ruta y referrer, y revisa los más frecuentes cada semana después del lanzamiento. Son las reglas que olvidaste.
  • Agrega una prueba en CI que recorra todo el inventario de URLs contra un deploy de preview y verifique un solo salto, el estado esperado y un 200 final:
while IFS=, read -r src expected; do
  out=$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$PREVIEW$src")
  code=${out%% *}; loc=${out#* }
  [ "$code" = "301" ] && [ "$loc" = "$PREVIEW$expected" ] || echo "FAIL $src -> $out"
  final=$(curl -s -o /dev/null -w '%{http_code}' "$loc")
  [ "$final" = "200" ] || echo "FAIL final $loc -> $final"
done < redirects.csv

Checklist

  1. Redirecciones almacenadas en una base de datos o en un archivo versionado con metadatos, no en configuración editada a mano.
  2. Una sola función de normalización compartida por escrituras y búsquedas.
  3. Bucles, cadenas, autorredirecciones y destinos muertos rechazados antes del deploy.
  4. Cadenas aplanadas para que cada petición sea un solo salto.
  5. Reglas exactas resueltas en el edge o desde un mapa compilado; patrones indexados por prefijo.
  6. Búsquedas en middleware filtradas antes con un filtro de Bloom o una caché.
  7. Comportamiento de falla abierta y snapshot de respaldo definidos.
  8. Lista de hosts permitidos para destinos y API de administración autenticada.
  9. Logs de redirecciones servidas y de 404 activos desde el día del lanzamiento.
  10. Prueba en CI que recorre todo el inventario de URLs.

Errores comunes

  • Poner miles de reglas en la configuración del framework y descubrir el límite de la plataforma durante el build del lanzamiento.
  • Usar códigos temporales (302/307) para cambios permanentes porque eran el valor por defecto de una función auxiliar.
  • Perder los query strings, lo que rompe el seguimiento de campañas en enlaces antiguos.
  • Actualizar contadores de visitas de forma síncrona dentro de la petición.
  • Borrar reglas antiguas cuando la migración "se estabiliza" sin revisar si todavía reciben tráfico.

Si estás planeando un cambio de plataforma y quieres diseñar la capa de redirecciones junto con el sitio nuevo, ese es el tipo de trabajo que hacemos en nuestros proyectos de diseño y desarrollo de páginas web, y para sistemas más grandes con sus propias herramientas de administración, a través de desarrollo de software a la medida.

Preguntas frecuentes

¿Cuántas redirecciones puedo poner en next.config.js?

Depende de la plataforma de hosting. En Vercel, la documentación de Next.js menciona un límite de 1,024 redirecciones, y otros tipos de rutas cuentan para el mismo tope. Más allá de unas mil, pasa a middleware con un almacén de datos.

¿Las redirecciones deben correr en el CDN o en la aplicación?

Las reglas de migración exactas y estables conviene resolverlas lo antes posible, en el edge o desde un mapa compilado. Las reglas que los editores cambian con frecuencia encajan mejor en middleware respaldado por un almacén clave-valor, para que los cambios apliquen sin deploy.

¿Un filtro de Bloom reemplaza la base de datos de redirecciones?

No. Solo responde "definitivamente no está" o "tal vez está". Sigues necesitando la búsqueda real para los positivos, y no ayuda con reglas de comodines o regex.