Caso real 03 · Modernización · child theme

Del tema padre a un child theme, sin romper producción

Una actualización del tema padre ya había borrado una vez el header y el footer a medida. Los migré a un child theme y encontré la causa real de los errores 500: PHP guardado en la base de datos y ejecutado con eval().

Caso real, cliente anonimizado. El nombre del cliente, las personas y los datos internos se omiten por confidencialidad. Las cifras salen de logs, Search Console, GA4 y del propio código. Si un dato de negocio no se midió, no aparece.
4 → 0plantillas a medida en el tema padre
2 → 0snippets PHP ejecutados desde la BD
−32 %CSS del header
−45 %CSS del footer
Contexto

El punto de partida.

El header, el footer y la plantilla de búsqueda eran componentes a medida que vivían dentro del tema padre. Cualquier actualización del tema podía sobrescribirlos, y ya había pasado una vez: desaparecieron en producción y hubo que volver a subirlos a mano.

El objetivo era simple de enunciar: que todo el código a medida sobreviva a las actualizaciones y esté en archivos versionables.

Síntomas observados

  • Al activar el child theme: HTTP 500 con «Cannot redeclare class».
  • functions.php del padre y del hijo, limpios. mu-plugins, limpios.
  • Ningún grep en el servidor encontraba el segundo include de las clases.
Diagnóstico

Hipótesis descartadas antes de tocar nada.

Lo que parecía obvio y no era. Descartar bien ahorra días de trabajo en la dirección equivocada.

Doble require en functions.php

Ya se habían retirado los require del padre y el error seguía.

Autoload de un plugin

Con los plugins desactivados en staging el error persistía.

OPcache con código antiguo

Tras resetear OPcache el comportamiento era idéntico.

Implementación

Qué hice, paso a paso.

Inventario del padre

search.php, componentes de header y footer, un hero sin uso y los require que los cargaban, más las rutas de assets que apuntaban al directorio del padre.

Rutas portables

Cambié las URLs de JS, imágenes y badges de get_template_directory_uri a get_stylesheet_directory_uri para evitar 404 al mover los archivos.

Encontrar el código fantasma

Dos snippets de un plugin de «insertar código» vivían en la base de datos y se ejecutaban con eval() antes que el tema. Hacían require de las clases desde la ruta antigua: por eso se declaraban dos veces.

El matiz que casi rompe todo

Esos snippets no solo cargaban las clases: también renderizaban el header y el footer. Al desactivarlos, desaparecían. Los reescribí sin el require y, en una segunda fase, moví el render al functions.php del child y borré los snippets.

Orden de activación seguro

Retirar los require del padre antes de activar el hijo, nunca después.

CSS más ligero

Minificación segura (solo comentarios y espacios) manteniendo el mismo nombre de archivo, porque el componente lo inyecta inline con file_get_contents.

Bajo el capó

El código que importa.

Fragmentos simplificados y sin datos del cliente, para que se vea el enfoque técnico real.

Encontrar PHP guardado en la base de datosbash
wp post list --post_type=wpcode --fields=ID,post_title,post_status
wp post meta get <ID> _wpcode_active
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_type='wpcode' AND post_content LIKE '%require_once%'" 
Carga de componentes desde el child themephp
// functions.php del child theme
foreach ( array( 'header', 'footer' ) as $component ) {
    $file = get_stylesheet_directory() . "/components/{$component}/{$component}.php";
    if ( file_exists( $file ) ) {
        require_once $file;
    }
}
add_action( 'wp_body_open', array( 'Site_Header_Component', 'render' ) );
add_action( 'wp_footer',    array( 'Site_Footer_Component', 'render' ) );
Decisiones

Por qué así y no de otra forma.

Dos fases en lugar de una

Primero estabilizar (snippets sin require), después migrar el render al tema. Reduce el riesgo de dejar la web sin header en producción.

Minificación conservadora

Sin reescribir reglas ni fusionar selectores: el ahorro viene de comentarios y espacios, y el riesgo visual es cero.

Resultado

Qué cambió, con datos verificables.

IndicadorAntesDespuésContexto
Plantillas a medida en el padre40a salvo de actualizaciones
Snippets PHP desde la BD20render en el child theme
CSS del header39,5 KB26,7 KB−32 %
CSS del footer12 KB6,6 KB−45 %

Cómo lo validé

  • wp eval comprobando que los shortcodes y las clases existen en runtime.
  • QA visual: header, footer, badges y buscador sin 404 en consola.
  • Misma secuencia repetida en producción tras revisar sus snippets.
Lo que me llevo

Cuando WordPress se comporta como si cargara código que no existe, reviso la base de datos antes que los archivos.

  • WordPress
  • Child theme
  • PHP
  • WP-CLI
  • Plugin de snippets
Para tu web

Checklist rápida: ¿te está pasando lo mismo?

  • ¿Hay código a medida dentro del tema padre?
  • ¿Hay PHP guardado en la base de datos (plugins de snippets, opciones)?
  • ¿Tus assets usan get_stylesheet_directory_uri?
  • ¿Tienes un orden de activación documentado?
¿Te suena?

¿Tienes un problema parecido?

Preguntas frecuentes

Dudas habituales sobre este tipo de problema.

¿Por qué no hay que tocar el tema padre?

Porque cualquier actualización lo sobrescribe. Lo que sea a medida debe vivir en un child theme o en un plugin.

¿Migrar a child theme cambia el diseño?

No debería. Cambia dónde vive el código y lo seguro que es actualizar.

¿Los plugins de snippets son un problema?

No por sí mismos, pero su código no está en el repositorio ni aparece en un grep. En sitios con varios desarrolladores conviene que la lógica importante viva en archivos.

WordPress · SEO técnico · IA

Cuéntame qué está frenando tu web.

Si ya tienes un WordPress en producción, reviso el contexto y te digo con honestidad si lo que necesitas es desarrollo, SEO técnico, rendimiento o automatización.