La web de la clínica llevaba semanas funcionando bien en el entorno de pruebas. Formularios de cita, páginas de especialidades, ficha de cada profesional. Todo cargaba, todo respondía. El equipo técnico del hosting confirmó que el paso a producción se había completado y el dominio definitivo ya resolvía correctamente. La primera impresión al abrir el sitio en el navegador era buena: la home cargaba entera, el menú funcionaba, las páginas principales respondían con normalidad. No había errores visibles. No había caídas.
Pero al día siguiente llegó el primer aviso. Un enlace dentro de una página de servicios abría una URL con el subdominio de staging: (entorno de pruebas). Una imagen en la cabecera de otra sección no cargaba, y al inspeccionar el origen, la ruta seguía apuntando al entorno antiguo. Luego apareció un formulario de contacto que enviaba datos a una dirección interna del servidor de desarrollo. El sitio estaba en producción, pero no del todo. Algo en aquella migración de staging había dejado piezas sin mover.
Esto no encajaba. Si el dominio resolvía bien y la home funcionaba, ¿por qué había recursos apuntando a un sitio que ya no debería existir en esa ruta?

Operario: WordPin
Expediente del caso: La migración de staging a URLs antiguas
Empresa o negocio: Clínica privada
Archivado en: Migraciones en WordPress
Nivel: Avanzado
Señales cruzadas tras la migración de staging
Lo primero fue catalogar dónde aparecían las referencias antiguas. No estaban en todas partes. La home cargaba limpia. Las páginas principales del menú también. Pero al entrar en páginas interiores, sobre todo en las de especialidades médicas y en las fichas de profesionales, empezaban a saltar URLs con el subdominio de staging en imágenes, enlaces internos y algún recurso incrustado.
Abrí DevTools en el navegador y revisé el código fuente de una de las páginas con problemas. En la pestaña de red, varios recursos devolvían errores o redirigían al entorno antiguo:
<img src="https://staging.clinicaejemplo.es/wp-content/uploads/2024/03/dra-martinez.jpg" alt="Dra. Martínez">
<a href="https://staging.clinicaejemplo.es/servicios/dermatologia/">Dermatología</a>Esas rutas no deberían seguir ahí. Si el traspaso del entorno de desarrollo a producción se hizo correctamente, todas las referencias internas deberían apuntar ya al dominio definitivo. La primera sospecha fue directa: caché.
La caché como primer sospechoso
El hosting tenía Varnish activo a nivel de servidor y el sitio usaba un plugin de caché de página. Si alguna versión antigua de las páginas había quedado almacenada con las URLs de staging, el navegador podía estar recibiendo contenido servido desde una capa de caché sin actualizar. Era la explicación más rápida y la más cómoda.
Purgué la caché del plugin desde el panel de WordPress. Pedí al hosting que vaciara Varnish. Probé en una ventana de incógnito, sin cookies ni sesión previa. Y también añadí un parámetro aleatorio a la URL para saltarme cualquier capa intermedia.
https://www.clinicaejemplo.es/servicios/dermatologia/?nocache=1La página seguía mostrando exactamente las mismas rutas de staging en el código fuente. La caché no era el problema. Si lo hubiera sido, al menos una de esas pruebas habría devuelto la versión limpia. Las cuatro devolvían lo mismo. Descarte confirmado.
La caché no inventa problemas. Muchas veces solo conserva los antiguos. Pero aquí ni siquiera estaba conservando nada: el contenido venía directamente con esas rutas.
Revisión de wp-config.php y constantes de entorno
La segunda pista iba por la configuración base. En muchas migraciones de entorno, las constantes WP_HOME y WP_SITEURL quedan definidas en wp-config.php apuntando al dominio de staging. Si eso no se corrige, WordPress genera todas las URLs internas con ese dominio aunque el servidor responda desde producción.
Accedí al archivo por SFTP y busqué ambas constantes:
define('WP_HOME', 'https://www.clinicaejemplo.es');
define('WP_SITEURL', 'https://www.clinicaejemplo.es');Las dos estaban correctas. Apuntaban al dominio definitivo. Comprobé también los valores de siteurl y home en la tabla wp_options y coincidían. WordPress sabía que estaba en producción. La configuración de entorno era limpia.
Demasiado evidente. Si WP_HOME y WP_SITEURL estuvieran mal, el problema sería global. Aquí el fallo solo aparecía en ciertas páginas y en ciertos campos. El patrón era selectivo, no masivo. Eso descartaba la configuración base como causa, pero confirmaba algo peor: las URLs de staging estaban incrustadas directamente en el contenido.
El rastro que dejó la migración de staging dentro de la base de datos
Si la configuración general era correcta y la caché no retenía nada antiguo, la referencia tenía que estar en otro sitio. Fui a mirar directamente la base de datos. Usé Adminer para buscar el subdominio de staging dentro de la tabla wp_posts:
SELECT ID, post_title, post_content
FROM wp_posts
WHERE post_content LIKE '%staging.clinicaejemplo.es%'
LIMIT 20;Devolvió 14 resultados. Páginas de servicios, entradas del blog y la página de contacto. Todas contenían URLs de staging dentro del contenido HTML almacenado. Eso explicaba por qué aparecían en el front: WordPress estaba sirviendo exactamente lo que tenía guardado.
Pero eso solo cubría una parte. Las imágenes rotas de las fichas de profesionales no estaban en post_content. Estaban en campos personalizados. Y el formulario que enviaba a staging tenía su configuración almacenada en las opciones del plugin. El problema no era un sitio del que tirar: eran varios.
Diagnóstico: contenido serializado con URLs de staging intactas
La causa real era que la sustitución de URLs durante la migración de staging no se completó. El equipo había hecho un reemplazo básico de cadenas en la base de datos, probablemente con una consulta SQL directa o con una herramienta que no procesaba datos serializados. Eso había funcionado para los campos de texto plano, pero había dejado intactas las referencias dentro de estructuras más complejas.
WordPress guarda configuraciones de plugins, widgets, opciones de tema y campos personalizados en datos serializados. Ese formato no almacena solo el texto: también guarda la longitud exacta de cada cadena. Si alguien cambia una URL con un reemplazo simple y no recalcula esa longitud, el dato puede quedar corrupto o, directamente, sin tocar.
Por eso algunas migraciones parecen limpias por fuera, pero siguen arrastrando rutas antiguas dentro de wp_options, wp_postmeta o tablas de plugins. En este caso, las URLs de staging no estaban en la configuración general del sitio. Estaban escondidas en campos que el primer reemplazo no había procesado bien.
Lo confirmé buscando en wp_options:
SELECT option_name, option_value
FROM wp_options
WHERE option_value LIKE '%staging.clinicaejemplo.es%';Ahí empezó a encajar todo. Aparecieron registros del tema activo, del plugin de formularios de cita y de un widget de imagen en el sidebar. No eran enlaces sueltos olvidados en una página. Eran piezas internas del sitio, guardadas en campos que el reemplazo original no había alcanzado.
option_name
theme_mods_clinica_child
appointment_form_settings
widget_media_imageCuando una URL aparece en una imagen, en un enlace interno y en un formulario al mismo tiempo, suelo desconfiar antes de la base de datos que de la caché. La caché repite lo que encuentra. La base de datos, en cambio, puede esconder una versión vieja del sitio dentro de una opción que nadie mira.
Que una página cargue no significa que todas sus piezas vengan del mismo sitio. Aquí la estructura principal respondía desde producción, pero los datos internos seguían mirando hacia staging.
El impacto no era solo visual. Las imágenes rotas afectaban a pacientes que buscaban información sobre especialistas antes de pedir cita. Los enlaces internos llevaban a zonas que no deberían estar visibles, o devolvían errores en mitad de la navegación. Y el formulario era el punto más delicado: si enviaba solicitudes al entorno antiguo, algunas citas podían no llegar al equipo de la clínica.
No era una URL rota cualquiera. Era una fuga silenciosa en el recorrido de un paciente que intentaba contactar. Para Google, además, el sitio mezclaba señales: enlaces internos rotos, recursos cargando desde un subdominio de pruebas y páginas que parecían terminadas, pero conservaban restos de una migración de staging incompleta.
Reemplazo controlado en toda la base de datos
La primera opción que parecía válida era lanzar un reemplazo masivo directamente sobre la base de datos con SQL. Un UPDATE con REPLACE() en todas las tablas relevantes habría cambiado las cadenas de staging por las de producción en minutos. Pero eso era exactamente lo que había fallado la primera vez. Una sustitución por texto plano dentro de campos serializados habría podido romper la estructura de datos.
Sustituir sin romper la serialización
La opción segura era usar una herramienta que supiera recalcular la longitud de las cadenas serializadas al hacer el reemplazo. WP-CLI tiene un comando específico para esto:
wp search-replace 'staging.clinicaejemplo.es' 'www.clinicaejemplo.es' --all-tables --precise --dry-runEl parámetro --dry-run permitía ver cuántos reemplazos iba a ejecutar sin tocar nada. El resultado fue el golpe sobre la mesa: 312 ocurrencias repartidas en 9 tablas.
Table Replacements
wp_posts 41
wp_postmeta 126
wp_options 89
wp_form_entries 18
wp_form_meta 22
otras_tablas 16La mayoría estaban en wp_posts, wp_postmeta y wp_options, justo donde el patrón ya apuntaba. Las páginas explicaban los enlaces antiguos. Los metadatos explicaban las imágenes de profesionales. Las opciones explicaban los formularios y widgets.
Ya no eran pistas separadas: eran el mismo fallo visto desde tres esquinas distintas. Ese fue el momento en que la migración de staging dejó de parecer un error puntual y pasó a ser una limpieza incompleta de base de datos.
El flag --precise era clave porque forzaba el procesado correcto de datos serializados, recalculando las longitudes para no romper las estructuras internas.
Antes de ejecutar nada, hice una copia completa de la base de datos. En una migración que ya había fallado una vez, no iba a lanzar un reemplazo masivo sin respaldo. Después ejecuté el comando sin --dry-run:
wp search-replace 'staging.clinicaejemplo.es' 'www.clinicaejemplo.es' --all-tables --preciseEl proceso terminó sin errores. Pero no bastaba con asumir que todo estaba limpio. Usé Better Search Replace para hacer una segunda pasada en modo simulación desde el panel de WordPress y confirmar que no quedaban restos. Devolvió cero coincidencias.
Después revisé manualmente las páginas que habían dado problemas al principio. Las imágenes de las fichas de profesionales cargaban desde el dominio correcto. Los enlaces internos de las páginas de servicios apuntaban a producción. Y el formulario de cita ya enviaba los datos a la URL definitiva.
Quedaba un último paso. Busqué en el código fuente del tema activo y en los archivos de configuración por si había alguna referencia hardcodeada al dominio de staging fuera de la base de datos. Encontré una línea en un archivo de plantilla del tema hijo que tenía una URL antigua en un enlace de la cabecera. La corregí manualmente. También revisé el archivo .htaccess y las reglas de redirección del servidor para confirmar que no hubiera reescrituras apuntando al entorno antiguo.
Un error parcial suele ser más peligroso que una caída total, porque deja demasiadas cosas funcionando mientras el fallo actúa por debajo.
Finalmente, purgué todas las capas de caché, regeneré el sitemap y solicité una nueva indexación en Google Search Console de las URLs afectadas. En las horas siguientes, el rastreo de Google dejó de encontrar referencias al dominio de staging. La migración de staging estaba realmente terminada.

💡 Qué he aprendido
Este caso me ha confirmado algo que ya intuía, pero que no había visto con tanta claridad: una migración de staging: (entorno de pruebas) no termina cuando el dominio resuelve. Termina cuando el último byte de la base de datos apunta al sitio correcto. El problema no fue la migración en sí, sino asumir que un reemplazo de texto plano iba a cubrir toda la base de datos de WordPress. No lo hizo. Y lo que dejó atrás no eran restos menores: eran formularios, imágenes y enlaces que el sitio usaba cada día.
He aprendido a no confiar en un solo método de reemplazo. Después de cualquier sustitución masiva, hace falta una segunda verificación con una herramienta distinta. Si la primera usa línea de comandos, la segunda debería ser visual. Si la primera es automática, la segunda debería incluir revisión manual de las páginas críticas.
También he confirmado que los campos serializados son el punto ciego de casi toda migración rápida. No gritan. No devuelven error. Simplemente siguen sirviendo lo viejo como si nada hubiera cambiado.
Cuaderno de WordPin
Forense WordPress
🧩 Notas técnicas
⬋⬋
La web de una clínica privada con formularios de cita presentaba URLs de staging en imágenes, enlaces internos y configuración de plugins tras el paso a producción. La configuración general del dominio era correcta, pero el contenido servido mantenía referencias al entorno antiguo en páginas interiores.
La causa fue un reemplazo de URLs que no procesó datos serializados en wp_options, wp_postmeta ni en las tablas de plugins. Se resolvió con WP-CLI search-replace con soporte de serialización, verificación con Better Search Replace y revisión manual de plantillas del tema hijo.
🔎 Pistas detectadas
⬋⬋
- Imágenes de fichas de profesionales cargando desde el subdominio de staging en lugar del dominio de producción.
- Enlaces internos en páginas de servicios que redirigían al entorno de desarrollo.
- Formulario de cita enviando datos a una URL del servidor de staging, provocando pérdida de solicitudes.
- Fallo selectivo: la home y páginas principales estaban limpias, pero las interiores conservaban rutas antiguas.
- Búsqueda en wp_options devolviendo configuraciones de tema, widgets y plugins con el dominio de staging en campos serializados.
🚫 Errores en WordPress detectados
⬋⬋
- Reemplazo de URLs por texto plano que no procesó campos serializados, dejando intactas las referencias de staging en opciones de plugins y widgets.
- URLs de staging incrustadas en post_content de 14 entradas y páginas, generando enlaces internos rotos hacia un entorno no público.
- Configuración del plugin de formularios apuntando al dominio antiguo, lo que provocaba que las citas de pacientes no llegaran al destino correcto.
- Línea hardcodeada en plantilla del tema hijo con URL de staging en la cabecera, fuera del alcance de cualquier reemplazo en base de datos.
- Ausencia de verificación post-migración que habría detectado los 312 restos de staging repartidos en 9 tablas.
📘 Glosario de WordPress
⬋⬋
- Datos serializados: formato de almacenamiento que incluye la longitud de cada cadena; un reemplazo sin recalcular esa longitud puede corromper el dato.
- WP-CLI search-replace: comando de línea que sustituye cadenas en base de datos respetando la serialización con el flag --precise.
- wp_options: tabla de WordPress que almacena configuraciones del sitio, tema, widgets y plugins, incluyendo campos serializados.
- wp_postmeta: tabla que guarda campos personalizados asociados a entradas y páginas, usada por plugins de campos avanzados.
- Entorno de staging: copia del sitio en un subdominio o servidor de pruebas, usada para desarrollar y validar cambios antes de producción.
📏 Mini guía práctica
⬋⬋
- Ejecuta el reemplazo de URLs con WP-CLI usando los flags --all-tables, --precise y --dry-run antes de aplicar cambios reales.
- Verifica con una segunda herramienta como Better Search Replace en modo simulación que no queden restos del dominio antiguo.
- Revisa manualmente los archivos del tema hijo y plantillas por si hay URLs hardcodeadas fuera de la base de datos.
- Comprueba las tablas wp_options y wp_postmeta buscando el dominio de staging con una consulta SELECT LIKE para detectar campos serializados intactos.
- Purga todas las capas de caché tras el reemplazo y solicita reindexación en Google Search Console de las URLs afectadas.
Deja una respuesta
El rastro oculto del crawl budget · Serpion 001
¿Qué es WordPress?, me preguntó un usuario · WordPin 000
Expedientes y Archivos