Convivir con lo heredado: reconocer, congelar y migrar sin sobresaltos
Vas a encontrarte proyectos de Pages durante años, así que lo útil no es despreciarlos sino saber operarlos y saber sacarlos de ahí cuando toque. Aprendemos a reconocer un proyecto heredado por sus huellas en el repositorio, a congelarlo para que deje de crecer, y a ejecutar la traducción exacta hacia Workers: del directorio de salida al bloque `assets`, de `context.next` al binding `ASSETS`, de la compilación de funciones al punto de entrada, con la trampa del orden de precedencia que rompe autenticaciones en silencio.
Hay una diferencia grande entre saber que algo es legado y saber trabajar con ello. Lo primero es una opinión y se adquiere leyendo una nota de producto; lo segundo es un oficio y se nota en cuánto tarda un equipo en tocar un proyecto que nadie ha abierto en dieciocho meses sin romperlo. Los proyectos de Pages son ahora mismo, y lo seguirán siendo durante años, una porción real del software que se mantiene en producción: sitios corporativos, documentaciones, paneles internos, campañas que sobrevivieron a sus campañas. Ese código no está mal escrito ni es despreciable; simplemente responde a un modelo que la plataforma ya superó. La tarea del ingeniero frente a él tiene tres tiempos muy distintos y conviene no mezclarlos: reconocerlo con rapidez, congelarlo para que deje de acumular deuda, y traducirlo cuando exista una razón de negocio que lo justifique, no antes ni bajo pánico.
- Reconocer un proyecto de Pages por sus huellas en el repositorio y en el manifiesto.
- Aplicar una política de congelación que impida que el proyecto heredado siga creciendo.
- Traducir cada pieza de Pages a su equivalente exacto en Workers con assets estáticos.
- Ejecutar una migración por fases con despliegue espejo y punto de retorno en cada paso.
Reconocerlo en treinta segundos
Un proyecto heredado se delata en el repositorio antes que en el panel. Estas cuatro señales bastan para clasificarlo sin preguntar a nadie, y conviene buscarlas en ese orden porque cada una implica un trabajo de migración distinto.
| Señal | Dónde mirar | Qué implica |
|---|---|---|
pages_build_output_dir |
El manifiesto de Wrangler | Proyecto de solo assets: la migración más simple |
Directorio functions/ |
La raíz del repositorio | Hay que compilar las funciones a un Worker |
_worker.js en la salida |
El resultado de la compilación | Modo avanzado: ya hay un Worker, solo hay que reubicarlo |
Comandos con pages |
Los scripts del package.json |
Detéctalos con grep -rn "wrangler pages" . | sort -u |
A esas señales se suman las de operación: un dominio terminado en pages.dev, un _routes.json en la carpeta pública, o un panel donde el proyecto aparece bajo la sección de Pages en lugar de la de Workers. Ninguna de ellas es un problema por sí misma; juntas te dicen exactamente cuánto trabajo hay por delante.
Congelar antes que migrar
El error más caro no es tardar en migrar: es seguir invirtiendo en el proyecto mientras decides. Cada capacidad nueva que se añade a un proyecto heredado es trabajo que habrá que traducir después, y se añade justo en el momento en que menos se piensa en la traducción. Por eso el primer movimiento no es técnico sino de política, y cuesta cero.
Nada nuevo dentro
Correcciones y contenido sí; capacidades nuevas no. Toda funcionalidad que no exista ya nace en un Worker aparte, nunca en functions/.
Contrato estable
Si otro servicio consume este proyecto, congela también su superficie pública. Un contrato quieto es lo que permite sustituir la implementación después.
Inventario escrito
Rutas, bindings, variables, dominios y redirecciones, en un documento. Lo que no está inventariado es lo que se rompe el día del cambio.
Fecha, aunque sea lejana
Una migración sin fecha no se hace nunca. Una fecha a doce meses vista con revisión trimestral basta para que el tema no desaparezca.
La traducción, pieza a pieza
Con el proyecto congelado e inventariado, la migración deja de ser una aventura y se vuelve una tabla de correspondencias. Requiere Wrangler en versión cuatro o superior; con eso, cada concepto de Pages tiene su equivalente exacto.
| En Pages | En Workers |
|---|---|
pages_build_output_dir |
El bloque assets con su directory |
wrangler pages deploy |
wrangler deploy |
wrangler pages dev |
wrangler dev |
Directorio functions/ |
Compilarlo y apuntar main al resultado |
_worker.js en la salida |
main hacia él y .assetsignore para no servirlo |
context.env y context.request |
Los argumentos env y request del manejador |
context.params |
El router explícito que elijas |
context.next |
El binding ASSETS y su método de recuperación |
_routes.json |
La opción de ejecutar el código antes que los assets |
Si el proyecto tiene funciones, el paso ineludible es compilarlas: el comando de Wrangler las convierte en un único script de Worker que conserva el enrutado por archivos, y ese script pasa a ser el punto de entrada. El comando se mantiene disponible de forma indefinida como puente, aunque la recomendación oficial para proyectos que quieran seguir con rutas por convención es adoptar un framework que las ofrezca de forma nativa.
npm install --save-dev wrangler@^4.0.0
npx wrangler pages functions build --outdir=./dist/worker/
npx wrangler deploy --dry-run
{
"name": "mi-app",
"compatibility_date": "2026-07-30",
"main": "./dist/worker/index.js",
"assets": {
"directory": "./dist/client/",
"binding": "ASSETS",
"run_worker_first": true
}
}
Pages evaluaba tus funciones antes que los archivos estáticos, y _routes.json servía para quitar rutas de esa evaluación. Workers hace lo contrario por defecto: si existe un archivo que encaja con la petición, lo sirve sin llegar a tu código. Un middleware que autenticaba, registraba o reescribía todas las peticiones dejará de ejecutarse para todas las rutas con archivo, y no verás ningún error: verás menos líneas de registro y una zona privada que sirve su HTML a cualquiera. La opción run_worker_first restaura el orden anterior. Revísala siempre que el proyecto tuviera middleware en la raíz.
El otro ajuste de traducción está en el cuerpo del código. Donde una función llamaba a context.next para delegar en el sitio estático, un Worker recupera el archivo a través del binding declarado, que es el mismo mecanismo con otro nombre.
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith("/privado/") && !(await autorizado(request, env))) {
return new Response("No autorizado", { status: 401 });
}
return env.ASSETS.fetch(request);
},
} satisfies ExportedHandler<Env>;
Migrar por fases, con retorno en cada paso
La migración no debería ser un evento sino una secuencia en la que cada paso es reversible y observable. El proyecto de Pages sigue en producción hasta el último momento, y el Worker nuevo se gana el dominio solo después de demostrar que sirve lo mismo.
flowchart TD A[proyecto de pages en produccion] --> B[congelar y hacer inventario] B --> C[worker espejo en dominio de prueba] C --> D[comparar rutas cabeceras y redirecciones] D -->|diferencias| C D -->|paridad verificada| E[mover el dominio al worker] E --> F[observar y mantener pages listo] F -->|todo estable| G[retirar el proyecto de pages] F -.incidencia.-> A style C fill:#89b4fa,color:#11111b style E fill:#f9e2af,color:#11111b style G fill:#a6e3a1,color:#11111b
La fase que más se subestima es la tercera, la comparación. No basta con que la portada cargue: hay que verificar las redirecciones con comodín, las cabeceras de caché por patrón, el comportamiento ante rutas inexistentes y, sobre todo, las rutas que pasaban por middleware. Un guion que recorra el inventario pidiendo cada ruta a los dos orígenes y comparando código de estado y cabeceras convierte esa fase en un problema medible en lugar de una inspección a ojo.
while read ruta; do
a=$(curl -s -o /dev/null -w "%{http_code} %{redirect_url}" "https://viejo.ejemplo.com$ruta")
b=$(curl -s -o /dev/null -w "%{http_code} %{redirect_url}" "https://nuevo.ejemplo.com$ruta")
[ "$a" = "$b" ] || echo "DIFIERE $ruta -> viejo=$a nuevo=$b"
done < inventario-rutas.txt
Ese guion de seis líneas hace algo que ninguna revisión manual consigue: convierte la paridad en un criterio binario y repetible. Puedes ejecutarlo tras cada ajuste, dejarlo en la tubería de integración mientras dure la convivencia y usar su salida vacía como la señal que autoriza mover el dominio. Amplíalo con las cabeceras que te importen —la de caché es la que más silenciosamente se pierde— y tendrás cubierto el noventa por ciento de lo que suele fallar.
La quinta fase, la retirada, tiene su propia disciplina. Mantén el proyecto de Pages desplegado y accesible por su dirección de origen durante un ciclo completo de facturación y observación, porque es tu vuelta atrás real: si aparece un problema grave, devolver el dominio es una operación de segundos frente a la alternativa de depurar bajo presión. Solo cuando hayan pasado semanas sin incidencias tiene sentido borrar el proyecto, y conviene hacerlo de forma explícita en vez de dejarlo ahí: un proyecto olvidado que sigue compilando en cada push es una fuente futura de confusión y, si tenía secretos, una superficie de riesgo que nadie está vigilando.
Merece la pena precisar qué significa exactamente que algo sea legado, porque la definición corriente —código antiguo, escrito por otros, que da miedo tocar— es sentimental y no sirve para decidir nada. Una definición operativa es mucho más útil: legado es el código cuyo modelo mental ya no coincide con el modelo mental de la plataforma sobre la que corre. Esa desalineación es la variable que de verdad predice el dolor, y explica hechos que la definición sentimental no explica. Explica por qué un sistema de quince años perfectamente alineado con su entorno se mantiene con calma y a coste previsible, mientras que un proyecto de dieciocho meses construido sobre un producto que acaba de congelarse ya duele: la edad no es la causa, la divergencia lo es. Y explica también por qué el dolor crece aunque nadie toque el repositorio, porque quien se mueve es la plataforma, no tú. De ahí se sigue la consecuencia práctica más importante de este nivel, que es dónde poner el esfuerzo. La tentación permanente es modernizar el código: reescribirlo con la sintaxis del año, cambiar de gestor de paquetes, actualizar dependencias hasta que el análisis estático quede en verde. Todo eso puede estar bien y no reduce ni un gramo la divergencia real, porque la divergencia no vive en la sintaxis: vive en supuestos estructurales, en cosas como qué se ejecuta antes, quién resuelve la ruta, dónde vive la configuración, qué es la unidad que se despliega. La inversión de precedencia entre assets y código que acabas de ver es el ejemplo perfecto y por eso rompe en silencio: no cambia ninguna línea de tu código, cambia el supuesto sobre el que tu código estaba escrito, y un supuesto roto no produce un error de compilación, produce una autenticación que ya no ocurre. Migrar bien, entonces, es sobre todo un ejercicio de arqueología de supuestos: enumerar qué daba por hecho el sistema antiguo sobre su entorno, verificar uno a uno si el nuevo entorno lo sigue garantizando, y escribir explícitamente los que ya no. Hazlo así y la migración se vuelve una lista finita de comprobaciones que puedes repartir, estimar y verificar. Sáltatelo y se convierte en lo que la mayoría de los equipos recuerda: un fin de semana largo, un cambio de dominio a ciegas y una semana de averías raras que nadie sabe atribuir, porque lo que falló nunca estuvo escrito en ninguna parte.
- Clona o abre un proyecto de Pages y clasifícalo con las cuatro señales: solo assets, con
functions/o en modo avanzado. Justifica la clasificación con la evidencia concreta que encontraste. - Escribe su inventario completo: rutas, bindings, variables, dominios, redirecciones y cabeceras por patrón. Ese documento es el criterio de aceptación de la migración.
- Redacta la política de congelación en tres líneas y decide dónde vivirá la próxima capacidad que alguien pida para ese proyecto.
- Ejecuta la traducción a un Worker con assets en un dominio de prueba, sin tocar producción. Si había middleware en la raíz, comprueba qué ocurre con y sin
run_worker_first. - Automatiza la comparación de paridad recorriendo el inventario y contrastando código de estado, cabeceras y redirecciones entre los dos orígenes. Solo cuando esa comparación salga limpia, mueve el dominio.