Saltar al contenido
Configuración

Buscar y reemplazar la URL en la base de datos de WordPress sin romper los datos serializados

Cambia la URL de un sitio WordPress en la base de datos sin dejar en blanco widgets, menús ni ajustes del tema. Usa el comando de WP-CLI que respeta los datos serializados.

Publicada

No ejecutes un UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl') a secas sobre una base de datos de WordPress. Cambiará el texto correctamente y destruirá tus ajustes al mismo tiempo, porque WordPress guarda arrays y objetos como cadenas serializadas de PHP, y ese formato anota la longitud en bytes de cada cadena que contiene. Un reemplazo directo por SQL reescribe el texto pero nunca toca la longitud registrada, y el valor deja de poder leerse.

Usa en su lugar una herramienta que entienda la serialización. En la línea de comandos, eso es wp search-replace. Haz primero una copia de la base de datos, ejecútalo en modo de prueba y luego en serio.

Qué se rompe realmente

Una cadena serializada de PHP tiene este aspecto:

s:18:"http://example.com"

El 18 es la longitud en bytes del texto que viene después. Cuéntalos: http://example.com tiene exactamente 18 caracteres.

Ahora imagina un ajuste del tema guardado así:

a:1:{s:4:"logo";s:18:"http://example.com";}

Ejecuta un reemplazo SQL a secas de http:// por https:// y obtienes:

a:1:{s:4:"logo";s:19:"https://example.com";}

Salvo que no. Lo que obtienes es esto:

a:1:{s:4:"logo";s:18:"https://example.com";}

La cadena ahora ocupa 19 bytes pero sigue afirmando que son 18. PHP lee la longitud declarada, avanza 18 bytes, no encuentra la comilla de cierre ni el punto y coma donde los espera, y se rinde. El array entero falla al deserializarse.

Por qué falla en silencio

Esta es la parte que hace que el fallo salga tan caro. WordPress no lanza un error ni se detiene. Cuando lee una opción, pasa el valor por su función auxiliar de deserialización, PHP devuelve false para los datos malformados y WordPress entrega a tu código un valor que se comporta como si la opción nunca se hubiera guardado. El plugin o el tema recurre entonces a su valor por defecto.

Así que el síntoma no es una página de error. El síntoma es:

  • Los widgets desaparecen de las barras laterales
  • Los ajustes del personalizador vuelven a los valores por defecto del tema
  • Los datos de maquetación de un constructor de páginas vuelven vacíos y la página se muestra en blanco
  • Las páginas de ajustes de los plugins parecen recién instaladas
  • Los menús pierden las ubicaciones que tenían asignadas

Nada en el escritorio avisa de que algo va mal. La fila sigue en la base de datos, llena de lo que parece texto correcto. Solo el prefijo de longitud está desviado uno o dos bytes. PHP sí emite un aviso cuando la deserialización falla, pero en un sitio en producción con los errores ocultos nadie lo ve. Activar WP_DEBUG junto con WP_DEBUG_LOG y con WP_DEBUG_DISPLAY en false deja esos avisos en un archivo de registro donde sí puedes leerlos.

El daño tampoco se reparte por igual. Los valores escalares sencillos —siteurl, home, el contenido normal de una entrada, una URL en su propia columna— sobreviven perfectamente a un reemplazo ingenuo. Solo se rompen los valores serializados. Por eso un reemplazo mal hecho a menudo parece haber funcionado a primera vista.

Revisa wp-config.php antes de tocar nada

Si wp-config.php define WP_HOME o WP_SITEURL, esas constantes ganan frente a lo que haya en la base de datos. Cambiar wp_options parecerá no servir absolutamente de nada. Busca líneas como estas antes de dar por hecho que el problema está en la base de datos:

define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );

O las actualizas con la nueva URL, o las eliminas y dejas que mande la base de datos.

Copia de seguridad primero, y en serio

Una serialización rota no siempre es fácil de revertir. Cuando el prefijo de longitud y el contenido dejan de coincidir, el valor original no se puede recuperar a partir del valor roto: no hay forma de saber cuál era la longitud correcta sin conocer la cadena original. La copia de seguridad es el único deshacer de verdad.

wp db export backup-before-replace.sql

O con mysqldump si no tienes WP-CLI:

mysqldump -u USER -p DBNAME > backup-before-replace.sql

Guárdala fuera del servidor si puedes. Una copia de seguridad en la raíz web es a la vez un punto de restauración y un problema de seguridad.

El comando seguro

El search-replace de WP-CLI deserializa cada valor, recorre la estructura decodificada, reemplaza dentro de ella y la vuelve a serializar con las longitudes correctas. Primero, en modo de prueba:

wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid

Lee el informe. Te dice cuántos reemplazos se harán por tabla y por columna. Si la lista de tablas tiene mala pinta, o una tabla que no reconoces acumula miles de coincidencias, para y míralo antes de ejecutarlo de verdad.

Después, ejecútalo:

wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only

Notas honestas sobre las opciones:

  • --all-tables cubre tablas que WordPress no registra por su cuenta, que es justo donde suelen vivir los datos de los plugins. Sin esta opción, algunas tablas de plugins se saltan por completo.
  • --skip-columns=guid importa. El guid es un identificador permanente para los lectores de feeds, no una URL activa. Reescribirlo puede hacer que tus suscriptores vean todo tu archivo como entradas nuevas.
  • Busca sin la barra final y sin las variantes de protocolo que no quieras tocar. Reemplazar example.com a secas también afectará a direcciones de correo y a cualquier mención dentro del texto de las entradas.

Si no puedes usar la línea de comandos, los plugins de migración que entienden la serialización hacen ese mismo trabajo de decodificación desde el escritorio. El comportamiento que buscas se describe explícitamente como manejo de datos serializados; si una herramienta no lo dice, da por hecho que no lo hace.

Qué tablas y columnas están implicadas

TablaColumnaRiesgo de serialización
wp_optionsoption_valueAlto — widgets, ajustes del tema, transients, casi todas las opciones de plugins
wp_postmetameta_valueAlto — maquetaciones de constructores de páginas, arrays de campos personalizados
wp_usermetameta_valueAlto — capacidades, preferencias de pantalla y de escritorio
wp_termmetameta_valueMedio — depende de los plugins
wp_postspost_content, guidBajo para el contenido, pero los atributos de shortcodes y el marcado de bloques llevan URLs
wp_commentscomment_content, comment_author_urlBajo
wp_linkslink_url, link_imageBajo, y a menudo sin uso

En multisitio, añade wp_blogs, wp_site, wp_sitemeta y las tablas por sitio wp_2_options, wp_3_options y así sucesivamente. Los cambios de URL en multisitio tienen reglas adicionales más allá de un buscar y reemplazar: trátalo como un trabajo aparte.

Los casos que search-replace no resuelve del todo

Siendo honestos con los límites:

JSON guardado dentro de la base de datos. Algunos constructores de páginas guardan la maquetación como JSON con las barras escapadas, así que tu URL aparece como https:\/\/example.com y no como https://example.com. Una búsqueda de la forma normal no la encontrará. Puede que necesites una segunda pasada sobre la cadena escapada. Pruébalo en una copia.

Valores codificados en base64. Si un plugin codifica sus ajustes antes de guardarlos, ningún reemplazo basado en texto puede ver la URL. Esos ajustes suelen tener que volver a introducirse desde la pantalla del propio plugin.

URLs escritas a mano en archivos. Todo lo que esté escrito en functions.php, en una plantilla del tema hijo o en una ruta de recurso fija no está en la base de datos y no se tocará. Revisa el directorio del tema con grep por separado.

Salida cacheada y generada. La caché de objetos, la caché de página y las copias del CDN siguen sirviendo las URLs antiguas incluso después de un reemplazo correcto. Vacíalas y vuelve a comprobarlo antes de dar por hecho que el reemplazo falló.

Si ya ejecutaste la consulta mala

Restaura la copia de seguridad. Esa es la respuesta, y merece la pena decirlo claro en lugar de ofrecer un procedimiento de reparación que casi nunca funciona.

Si no hay copia de seguridad, el camino recuperable es estrecho: los valores que nunca estuvieron serializados están bien, y los serializados rotos hay que reconstruirlos a mano o reiniciarlos volviendo a guardar la pantalla de ajustes correspondiente. Volver a guardar un área de widgets, reasignar una ubicación de menú o reintroducir los ajustes de un plugin escribe un valor nuevo y correctamente serializado encima del roto. Es tedioso, pero funciona, y es mejor que dejar filas corruptas ahí donde confundirán a la siguiente persona que mire el sitio.

Para generar las sentencias correctas antes de ejecutar nada, constrúyelas con la herramienta de buscar y reemplazar SQL para WordPress: te muestra qué columnas se pueden tocar con SQL a secas y cuáles exigen un tratamiento que respete la serialización, así que ves la separación antes de comprometerte con un comando.

FAQ

Preguntas

¿Por qué un REPLACE de SQL a secas rompe un sitio WordPress?

Porque WordPress guarda arrays y objetos como texto serializado de PHP, y ese formato anota la longitud en bytes de cada cadena que contiene. Un REPLACE directo cambia el texto pero deja intacto el número de longitud antiguo. PHP se niega entonces a deserializar el valor, WordPress recurre a un valor por defecto y los ajustes aparecen vacíos en lugar de incorrectos.

¿Cuál es la forma más segura de cambiar la URL de un sitio WordPress en la base de datos?

Haz una copia de seguridad completa de la base de datos y después ejecuta el comando de WP-CLI wp search-replace, primero en modo de prueba. WP-CLI deserializa cada valor, reemplaza dentro de la estructura ya decodificada y vuelve a serializar con las longitudes corregidas. Los plugins de migración que entienden la serialización hacen lo mismo desde el escritorio si no tienes acceso a la línea de comandos.

¿Qué tablas guardan la URL antigua?

Principalmente wp_options, wp_posts, wp_postmeta, wp_usermeta, wp_termmeta y wp_comments. El peligro de la serialización está en wp_options y en cualquier columna meta_value, porque los widgets, los ajustes del tema, los transients y los datos de los maquetadores se guardan como arrays. En multisitio, wp_blogs, wp_site y wp_sitemeta también contienen el dominio.

¿Cambiar la base de datos arregla la URL si wp-config.php la define?

No. Si wp-config.php define WP_HOME o WP_SITEURL, esas constantes tienen prioridad sobre lo que haya en siteurl y home dentro de wp_options. Editar la base de datos no cambia nada visible hasta que actualices o elimines las constantes. Revisa wp-config.php primero: esa única línea explica muchísimas migraciones fallidas.

¿Debo reemplazar también la columna guid?

No. El guid es un identificador permanente para los lectores de feeds, no una URL en funcionamiento, y reescribirlo puede hacer que los suscriptores vean todas las entradas antiguas como nuevas. Pasa skip-columns con guid al ejecutar WP-CLI. La única excepción habitual es un sitio que nunca tuvo feeds públicos ni suscriptores.

¿Cómo sé si un valor quedó roto tras un reemplazo mal hecho?

Los valores serializados rotos se ven normales en la base de datos pero vuelven vacíos en WordPress. Los widgets desaparecen, los ajustes del personalizador se reinician y las opciones de los plugins vuelven a sus valores por defecto sin ningún mensaje de error. Activar WP_DEBUG y WP_DEBUG_LOG saca a la luz el aviso de deserialización. Comparar la longitud declarada con el número real de bytes lo confirma directamente.