Un código base WordPress legacy es aquel en el que nadie puede predecir qué romperá un cambio. La edad no es el problema: lo es la falta de medición. Esta guía trata de producir primero esas mediciones y después sustituir el código en un orden donde cada paso se pueda deshacer.
Los sitios de los que hablamos se reconocen enseguida: construidos entre 2015 y 2020, un tema clásico con un functions.php que pasó de las dos mil líneas, varios plugins a medida sin repositorio upstream y una tabla wp_options que nadie ha mirado desde el lanzamiento. Normalmente siguen funcionando. Lo que ha dejado de funcionar es la capacidad de cambiarlos sin sustos.
WordPress 7.1.1 salió el 17 de septiembre de 2026. La distancia entre esa versión y un stack de 2018 rara vez está en el núcleo. Está en la versión de PHP de debajo, en los scripts que encola el tema y en una base de datos que lleva años acumulando filas de plugins que ya no existen.
Conoce más sobre el desarrollo WordPress profesional en WPPoland.
1. Cómo auditar un WordPress legacy
Empieza por los números que se pueden leer en un sitio en producción, no por una opinión sobre el código.
Versión mínima y recomendada de PHP para WordPress
El suelo y la recomendación son cosas distintas. El trunk de WordPress sigue fijando $required_php_version = '7.4' en src/wp-includes/version.php, mientras que la página de requisitos recomienda PHP 8.3 o superior y MariaDB 10.11 o MySQL 8.0. Es decir: el núcleo arranca en 7.4, y arrancar en 7.4 significa hacerlo sin parches.
Lo que decide la migración son las fechas de php.net, no un porcentaje de mejora prometido:
| Rama PHP | Situación a 20 de septiembre de 2026 |
|---|---|
| 7.4 | Fin de vida el 28 de noviembre de 2022 |
| 8.0 | Fin de vida el 26 de noviembre de 2023 |
| 8.1 | Fin de soporte de seguridad el 31 de diciembre de 2025 |
| 8.2 | Soporte de seguridad hasta el 31 de diciembre de 2026 |
| 8.3 y posteriores | Con soporte activo |
Un plan que apunte a 8.2 tiene unos tres meses de margen. Apunta a 8.3 o 8.4.
Un sitio en 7.4, 8.0 u 8.1 no recibió ninguna versión nueva cuando se corrigieron las vulnerabilidades críticas de PHP del 21 de octubre de 2026. Los pasos para ese sitio están en qué hacer con PHP2Shell en un sitio WordPress.
Cómo registrar funciones obsoletas en producción sin WP_DEBUG
Esta es la parte que la mayoría de auditorías hace mal. El núcleo dispara do_action( 'deprecated_function_run', ... ) dentro de _deprecated_function() antes de comprobar WP_DEBUG; lo único condicionado es la llamada a trigger_error(). Lo mismo vale para deprecated_hook_run, deprecated_argument_run, deprecated_file_included, deprecated_class_run y deprecated_constructor_run. Un mu-plugin pequeño te da un inventario real del tráfico en vivo sin mostrar nada en pantalla:
<?php
// mu-plugins/deprecation-log.php
$hooks = array(
'deprecated_function_run',
'deprecated_hook_run',
'deprecated_argument_run',
'deprecated_file_included',
'deprecated_class_run',
'deprecated_constructor_run',
);
foreach ( $hooks as $hook ) {
add_action(
$hook,
static function () use ( $hook ) {
error_log( $hook . ' ' . wp_json_encode( func_get_args() ) );
},
10,
4
);
}Déjalo corriendo un ciclo de negocio completo. Una importación semanal o una página de informe trimestral tocan código deprecado que ningún rastreo del front-end alcanza.
Cómo verificar los checksums de WordPress con WP-CLI
wp core verify-checksums y wp plugin verify-checksums comparan lo instalado con los hashes de WordPress.org. En un sitio con una década de historia esto encuentra casi siempre un plugin parcheado a mano por alguien que ya no está en el equipo. Esos archivos son los que vuelven atrás en la siguiente actualización, y son los primeros que deben entrar en control de versiones.
¿Cuántas versiones de jQuery carga un WordPress antiguo?
Antes de culpar a jQuery, cuenta cuántas copias hay. El núcleo registra exactamente una: script-loader.php declara jquery-core en la versión 3.7.1 y jquery-migrate en la 3.4.1, con jquery como alias que depende de ambas. Si en una página aparecen varias versiones, vienen de plugins y temas que encolan su propia copia con otro handle, o que escriben una etiqueta <script src> a pelo en el footer. wp_scripts()->queue te dice cuál es cuál en un minuto. Desregistrar la jQuery del núcleo para forzar una copia de CDN es justo el cambio que rompe el escritorio: deja el handle del núcleo en paz y elimina los duplicados.
jquery-migrate merece una decisión aparte. Quitarlo de la cola es una línea, y si entonces la consola se llena de errores, esos errores son la lista real de patrones de jQuery 1.x que quedan en el tema.
Escaneo de compatibilidad PHP con PHPCompatibilityWP
PHPCompatibilityWP existe para que los sniffs no marquen los polyfills que el núcleo ya trae:
vendor/bin/phpcs -p wp-content/themes wp-content/plugins \
--standard=PHPCompatibilityWP \
--extensions=php \
--runtime-set testVersion 8.3-Sin testVersion el resultado dice muy poco. Con él obtienes una lista de archivos ordenada por lo que te separa de la versión a la que quieres llegar.
El inventario que deberías tener al cerrar la primera semana son cuatro columnas: versión de PHP actual, deprecaciones por endpoint, archivos que fallan el checksum y errores de PHPCS por plugin. Todo lo que viene después se prioriza desde esa tabla.
2. Cómo refactorizar WordPress con el patrón strangler fig
No intentes cambiarlo todo de una vez. El strangler fig de Martin Fowler, escrito en 2004 y revisado en agosto de 2024, es la única estrategia de reescritura que mantiene en pie un sitio que factura: pon el sistema nuevo al lado del viejo, manda una porción del tráfico al nuevo y deja que el viejo encoja hasta que no quede nada que cortar.
WordPress ofrece dos costuras naturales para esto, y las dos están en el núcleo.
Fase 1: actualizar el núcleo y la infraestructura
Antes de mover plantillas, deja la infraestructura en condiciones: PHP en una rama con soporte, WordPress en la última estable, MySQL 8.0 o MariaDB 10.11 según la página de requisitos, HTTPS en todo el sitio y copias de seguridad que alguien haya restaurado alguna vez de verdad.
Fase 2: migrar plantillas ruta a ruta
La primera costura es template_include. Es un filtro, así que una sola ruta puede pasar al código nuevo mientras todas las demás siguen entrando por las plantillas viejas. Con eso basta para llevar la página de contacto, la sección de empleo o un tipo de contenido a una plantilla reescrita con sus propios activos, y para devolverla a su sitio con una línea si cae la conversión.
La segunda es el soporte híbrido de plantillas de bloques. locate_block_template(), en src/wp-includes/block-template.php, sale antes de tiempo salvo que current_theme_supports( 'block-templates' ) sea cierto, y cuando ese soporte está declarado solo considera plantillas de bloques con especificidad igual o mayor que la plantilla PHP que la jerarquía ya encontró. En la práctica:
add_action(
'after_setup_theme',
static function () {
add_theme_support( 'block-templates' );
}
);A partir de ahí, un archivo templates/page-contact.html dentro del tema clásico se queda con esa ruta exacta. single.php, archive.php y el resto siguen igual. Este es el mecanismo que convierte “migrar a bloques” en una serie de entregas pequeñas en lugar de un fin de semana con un plan de vuelta atrás que nadie ha probado.
Ordena las porciones por riesgo, no por entusiasmo. Una buena primera porción es una plantilla con tráfico real, un resultado medible y ningún checkout dentro. Una mala primera porción es la portada, porque te da el mayor radio de impacto posible el día en que menos conoces el código nuevo.
Fase 3: retirar las plantillas PHP antiguas
Estrangula las plantillas PHP antiguas hasta que el sitio entero funcione sobre el stack moderno. Dos reglas hacen que esto se sostenga. Cada porción se lleva sus propios activos, para que la plantilla nueva no herede sin querer la hoja de estilos global antigua. Y el código viejo se borra en la misma entrega que lo jubila, porque una plantilla muerta que se queda en el repositorio la va a editar alguien antes de seis meses.
3. Cómo limpiar la base de datos de un WordPress antiguo
Casi todos los informes de “la base de datos va lenta” en un WordPress antiguo se reducen a tres cosas, y solo una tiene que ver con el tamaño.
Cómo reducir el autoload de wp_options
El autoload es el que se paga en cada petición. Desde WordPress 6.6 la columna autoload ya no guarda solo yes y no. La nota de desarrollo de la Options API introdujo on, off, auto-on, auto-off y auto, y wp_filter_default_autoload_value_via_option_size(), en src/wp-includes/option.php, se niega a autocargar cualquier valor serializado que supere el filtro wp_max_autoloaded_option_size, con 150000 bytes por defecto. Esa protección actúa cuando la opción se escribe. No hace nada con el blob que un plugin desinstalado dejó ahí en 2019.
Lee la lista real con SQL, no con el panel de un plugin:
SELECT option_name, LENGTH(option_value) AS bytes, autoload
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto')
ORDER BY bytes DESC
LIMIT 25;Esos cuatro valores son los que wp_load_alloptions() trata como autocargados. Importa, porque wp option list --autoload=on --format=total_bytes en WP-CLI solo coincide con autoload='on' OR autoload='yes'. En un sitio con 6.6 o posterior, donde las opciones nuevas entran como auto, ese comando informa de una huella de autoload menor que la que pagas en cada carga de página. Usa la consulta, o lee el número de WP-CLI sabiendo qué deja fuera.
Cómo limpiar la postmeta huérfana
La postmeta huérfana es un recuento, no un misterio. Cuéntala antes de decidir que importa:
SELECT COUNT(*)
FROM wp_postmeta pm
LEFT JOIN wp_posts p ON p.ID = pm.post_id
WHERE p.ID IS NULL;Las filas sin post superviviente se pueden borrar, y de todas formas antes se hace un volcado de la base de datos. La meta que pertenece a un plugin que ya retiraste es una pasada aparte y más cuidadosa: agrupa por meta_key, contrasta los prefijos con los plugins que sigues usando y borra por clave.
Las revisiones antiguas son el tercer bloque, y el post_type se escribe en inglés aunque el sitio esté en español:
DELETE FROM wp_posts
WHERE post_type = 'revision'
AND post_date < DATE_SUB(NOW(), INTERVAL 6 MONTH);Índices de wp_postmeta, InnoDB y utf8mb4
Los índices son la parte que nadie lee. src/wp-admin/includes/schema.php da a wp_postmeta una clave primaria sobre meta_id, una clave post_id y una clave meta_key recortada a 191 caracteres, porque utf8mb4 usa cuatro bytes por carácter y el límite histórico de índice era de 767 bytes. No hay índice compuesto sobre (post_id, meta_key). Una consulta que filtra por ambas columnas, que es lo que hace una plantilla de archivo cargada de meta en cada fila, no puede usar un solo índice para las dos. En un sitio grande la solución es un índice compuesto añadido por migración, no otra capa de caché encima.
Dos comprobaciones más van en la misma pasada. Confirma que todas las tablas son InnoDB, porque una tabla MyISAM heredada de una importación de la época de MySQL 5.1 bloquea al escribir e ignora las transacciones. Y confirma que el juego de caracteres es utf8mb4: el núcleo elige utf8mb4_unicode_520_ci en determine_charset() cuando el servidor lo admite, pero una base creada antes de WordPress 4.2 y migrada con mysqldump seguirá en utf8 y seguirá perdiendo en silencio los caracteres fuera del plano básico.
4. Cómo modernizar CSS y JavaScript en un tema legacy
El front-end de un WordPress legacy no suele ir lento por el framework que usa. Va lento porque nada en el tema sabe qué encola lo demás.
Cómo dividir un style.css monolítico en módulos
Un tema legacy con un único style.css editado a mano y sin bundler no tiene forma de entregar un cambio acotado. Que la salida sea Tailwind, CSS modules o propiedades personalizadas importa mucho menos que poder responder a “qué selectores necesita esta plantilla”. Una estructura que permite responder a eso se parece a esto:
styles/
├── critical.css (en línea en <head>)
├── components/
│ ├── header.css (en todas las páginas)
│ ├── blog.css (solo en páginas de blog)
│ └── product.css (solo en páginas de producto)
└── utilities.css (clases utilitarias compartidas)Una vez existe un build, la carga de activos por plantilla es posible, y esa carga por plantilla es lo que mueve la ruta de renderizado, no la elección de framework.
Cómo cargar scripts con defer y async en WordPress
Usa la estrategia de carga que el núcleo ya trae. Desde WordPress 6.3, wp_enqueue_script() acepta un array en el último parámetro:
wp_enqueue_script(
'site-main',
get_theme_file_uri( 'build/main.js' ),
array(),
filemtime( get_theme_file_path( 'build/main.js' ) ),
array(
'strategy' => 'defer',
'in_footer' => true,
)
);La trampa está documentada en la nota de desarrollo de 6.3 e implementada en WP_Scripts::filter_eligible_strategies(): el núcleo recorre el árbol de dependencias y, si algún script encolado que depende del tuyo no está también diferido, el tuyo se emite como bloqueante. Puedes poner defer en un handle, no ver ningún atributo defer en el HTML y no tener nada en el log. El núcleo escribe data-wp-strategy en la etiqueta siempre que se registró una estrategia diferida, se haya degradado o no, así que buscar solo ese atributo devuelve todos los scripts diferidos. La señal de degradación es una etiqueta con data-wp-strategy y sin el defer o async correspondiente. Un solo plugin antiguo que encole un script bloqueante dependiente de tu bundle basta para tirar abajo el cambio entero.
Ese mismo archivo rechaza además una estrategia sobre un handle alias, que es por lo que wp_script_add_data( 'jquery', 'strategy', 'defer' ) no hace nada útil: jquery no tiene src.
Cómo eliminar jQuery de un tema legacy
El proceso es gradual y se apoya en el inventario de la sección 1:
- Listar los handles encolados con
wp_scripts()->queuey separar la jQuery del núcleo de las copias duplicadas. - Identificar qué plugins dependen del handle
jquery. - Quitar
jquery-migratede la cola y tratar los errores de consola como la lista de trabajo. - Sustituir las funcionalidades simples por JavaScript nativo.
- Retirar la dependencia cuando ya no quede nada que la pida.
5. Cómo proteger un WordPress antiguo
El código legacy no es inseguro por antiguo. Es inseguro porque las superficies que expone se configuraron cuando los valores por defecto eran otros.
Herramientas de escaneo de seguridad para WordPress
Vale más una verificación de checksums programada, un registro de deprecaciones y alertas de integridad de archivos que un escáner que devuelve una puntuación de severidad. Las herramientas habituales para el resto de la pasada son PHPStan para análisis estático de PHP, npm audit para dependencias JavaScript y WPScan para vulnerabilidades ya publicadas.
Cómo restringir la API REST de WordPress
La API REST no se puede apagar, y los snippets que dicen hacerlo son peores que no tocar nada: el núcleo la usa para el editor de bloques, Site Health y las contraseñas de aplicación. Lo que sí puedes hacer es exigir autenticación para todo lo que no sea explícitamente público:
add_filter(
'rest_authentication_errors',
static function ( $result ) {
if ( ! empty( $result ) || is_user_logged_in() ) {
return $result;
}
return new WP_Error(
'rest_not_logged_in',
__( 'El acceso a la API REST requiere autenticación.' ),
array( 'status' => 401 )
);
}
);Después prueba sin sesión iniciada las partes del sitio que dependen de ella: formularios de comentarios, bloques de búsqueda, formularios de contacto montados sobre endpoints REST y cualquier consumidor headless. En un sitio corporativo de marketing este filtro suele ser inofensivo. En uno con una función interactiva para visitantes anónimos la romperá, y es mejor descubrirlo en staging.
Sabe qué está ya restringido antes de añadir reglas. El endpoint de usuarios es un susto recurrente, pero WP_REST_Users_Controller aplica has_published_posts en las peticiones de colección siempre que quien llama no supera current_user_can( 'list_users' ), lo que cubre tanto a visitantes sin sesión como a cuentas con pocos permisos. Los tipos de contenido que acepta son get_post_types( array( 'show_in_rest' => true ) ), no los públicos, y las dos listas no coinciden. Así que el endpoint devuelve autores que han publicado en un tipo expuesto en REST, no todas las cuentas. La enumeración de autores en un sitio corporativo suele escaparse por las redirecciones de /?author=1 y por los archivos de autor, no por REST.
Las contraseñas de aplicación, además, son solo para HTTPS: wp_is_application_passwords_supported() devuelve is_ssl() || 'local' === wp_get_environment_type(). Un sitio antiguo que todavía sirve el escritorio con contenido mixto sobre HTTP plano no las ofrece, y por eso las integraciones en esos sitios acaban compartiendo la contraseña real de un administrador. Arreglar el certificado es la corrección de seguridad; la contraseña de aplicación es la consecuencia.
Cómo actualizar las dependencias de un WordPress antiguo
Cada biblioteca de terceros se actualiza o se sustituye:
- Librerías PHP con vulnerabilidades publicadas
- Componentes JavaScript con CVE activos
- Fuentes y activos cargados desde CDN de terceros
- Integraciones contra APIs ya retiradas
Haz a la vez el inventario de plugins, porque cada plugin que retiras es una superficie que dejas de mantener.
6. Cómo migrar a Gutenberg desde un tema clásico
Compatibilidad tema legacy + Gutenberg
Para temas construidos entre 2015 y 2018, el soporte se habilita por partes. add_theme_support( 'block-templates' ) es el interruptor que abre la migración plantilla a plantilla de la sección 2; el resto son ajustes del editor:
add_action('after_setup_theme', function() {
// Soporte básico de bloques
add_theme_support('wp-block-styles');
add_theme_support('align-wide');
add_theme_support('responsive-embeds');
// Paleta de colores del tema legacy
add_theme_support('editor-color-palette', [
['name' => 'Primario', 'slug' => 'primary', 'color' => '#1a1a2e'],
['name' => 'Secundario', 'slug' => 'secondary', 'color' => '#16213e'],
]);
});El esquema de theme.json va por la versión 3. No existe una versión 4, así que cualquier ejemplo que la declare hará que el núcleo ignore el archivo.
Cómo migrar de Elementor o Divi a Gutenberg
La migración desde Elementor, Divi o Visual Composer a Gutenberg sigue un proceso estructurado:
- Inventario: documentar cada página y su constructor actual
- Priorización: empezar por las páginas con más tráfico
- Conversión: recrear los layouts con patrones de bloques
- Verificación: comparar visual y funcionalmente
- Activación: cambiar página a página con redirecciones de prueba
- Limpieza: desactivar el page builder antiguo cuando todas las páginas estén migradas
Comprueba la versión del constructor contra la API de plugins de wordpress.org (api.wordpress.org/plugins/info/1.0/elementor.json para Elementor, que a 12 de octubre de 2026 publica la 4.3.4 como estable) y no contra el readme.txt del repositorio, que se queda desfasado.
7. Cómo medir el éxito de una modernización WordPress
No prometas porcentajes antes de medir. Lo que sí se puede fijar antes de empezar es el instrumento con el que se va a comprobar cada cosa, y el valor de partida del sitio tal como está hoy.
| Qué se mide | Instrumento | Cuándo se toma |
|---|---|---|
| LCP e INP de campo | Informe CrUX o RUM propio | Antes de la primera porción y 28 días después |
| TTFB | WebPageTest o los logs del servidor | Por plantilla, no como media del sitio |
| Consultas SQL por página | Query Monitor | En las plantillas con tráfico, con caché de objetos activa |
| Bytes autocargados | La consulta SQL de la sección 3 | Antes y después de la limpieza de wp_options |
| Errores PHP por día | Registro de errores del servidor | Durante todo un ciclo de negocio |
| Deprecaciones por endpoint | El mu-plugin de la sección 1 | Durante todo un ciclo de negocio |
Del lado del negocio, la única cifra honesta antes de empezar es la que ya tienes: tráfico orgánico, conversión y coste de mantenimiento actuales. La comparación se hace después, contra esa misma línea base y con la ventana de medición decidida de antemano.
8. Auditoría y modernización de WordPress con WPPoland
En WPPoland trabajamos el rescate de sitios WordPress, y siempre por este orden.
Due diligence y auditorías: entregamos el inventario técnico de la sección 1, con prioridades y el coste estimado de cada intervención.
Despliegues incrementales: modernizamos por etapas, cada una con su criterio de aceptación y su forma de volver atrás.
Objetivos medidos, no prometidos: acordamos qué métrica se mueve, con qué instrumento se mide y en qué ventana, antes de tocar el código. Lo que no se pueda medir no entra en el alcance.
Transferencia de conocimiento: documentamos el proceso y formamos a tu equipo para que mantenga el código modernizado por su cuenta.
9. Por dónde empezar a modernizar un WordPress legacy
Un sitio WordPress legacy no es automáticamente un lastre. Lo es la deuda técnica que nadie gestiona, y la diferencia entre ambos casos está en si alguien puede decir cuánto cuesta un cambio antes de hacerlo.
La secuencia de esta guía es deliberada. Mide la brecha de PHP contra las fechas de php.net, recoge deprecaciones del tráfico real con los hooks que el núcleo ya dispara, verifica los archivos contra los checksums y después estrangula las plantillas ruta a ruta con template_include o block-templates. Arregla el autoload y los índices de postmeta, porque están en cada petición. Arregla la carga de scripts, porque el núcleo te da el mecanismo y los plugins antiguos te lo quitan en silencio. Todo lo demás es una preferencia mientras eso no esté hecho.
Si el sitio sigue con un stack de 2018, el primer entregable no es código. Es un documento: versión de PHP, registro de deprecaciones, fallos de checksum, lista de plugins con responsable y las plantillas que cargan con el tráfico.
¿Tu sitio corporativo está atrapado en 2018? Escribe a WPPoland para una auditoría y empieza por el inventario, no por la reescritura.
Recursos relacionados
- Desarrollo WordPress profesional - Modernización empresarial
- Optimización de velocidad WordPress - Rendimiento post-modernización
- Auditoría de seguridad WordPress - Seguridad para código legacy
- Rediseño WordPress - Modernización visual completa
- Migración a Astro y Next.js - Arquitecturas modernas post-refactorización







