Diseño y desarrollo de mikejayduran.com (WordPress, bilingüe ES/EN)

Diseñé y desarrollé por completo mikejayduran.com, mi sitio personal bilingüe, sobre WordPress y el tema Blocksy (responsive y ligero). El diseño visual pasó por tres iteraciones completas antes de cuajar, todas persiguiendo lo mismo: una interfaz cuidada que se aparta del camino y cede el protagonismo a los artículos, que son la razón de ser del sitio.

Ese minimalismo no es solo de fachada, también rige por dentro. Mantengo los plugins al mínimo y resuelvo la mayoría de las funciones con snippets propios de PHP y JavaScript, porque cada plugin es superficie de mantenimiento, de rendimiento y de ataque que hay que justificar. Esos snippets cubren la generación de Open Graph y Twitter Cards, noindex selectivo, precarga de recursos críticos, la conversión de Markdown a bloques de Gutenberg (escribo en Markdown y no quiero pelearme con el editor al importar), y varios ajustes de seguridad: el acceso de administrador vive tras rutas alias y wp-login.php devuelve un 404 a quien no tenga sesión, con XML-RPC y los comentarios expresamente desactivados. Lo atraviesa todo un criterio de privacidad: tipografías autoalojadas en lugar de Google Fonts, avatares locales en vez de Gravatar, y ni una sola cookie de terceros.

El filtrado por etiquetas de la portada funciona por query string, deliberadamente, para que las combinaciones de tags sean cacheables en servidor con WP Super Cache en vez de ejecutar PHP en cada visita. Y como un caché frío solo beneficia al segundo visitante, al publicar un artículo un proceso en segundo plano recorre por WP-Cron las combinaciones más frecuentes y las precalienta, de modo que quien llegue primero ya encuentre la página servida desde caché. El sitio resulta notablemente más snappy.

El problema técnico más interesante que resolví fue un bug de redirección: la versión en inglés (/en/) devolvía la portada en español sin dar un solo error, ni en pantalla ni en consola. Los fallos silenciosos son los peores de su clase, porque nadie los reporta: el visitante angloparlante asume que la versión en inglés no existe y se va. Y un 301 desde /en/ además le enseña a Google que esa URL no merece indexarse por separado, así que el bug trabajaba activamente contra el bilingüismo del sitio.

Lo primero era saber si aquello era una redirección real del servidor o un espejismo del cliente. Descarté caché del navegador, cookies y snippets uno por uno. La confirmación llegó del panel Network del navegador, exportando el tráfico a un archivo HAR y filtrando por código de estado: un 301 Moved Permanently desde /en/ con cabecera Location: /es/, seguido de una segunda petición encadenada que devolvía 200 OK en español. Dos peticiones donde debía haber una, que es justo lo que produce una redirección de servidor y no una cookie ni un caché.

Lo que el HAR no podía decirme era por qué. Una petición a WordPress no es un paso atómico, sino una secuencia larga y ordenada de hooks a los que cualquiera de las decenas de piezas activas puede engancharse. Así que instrumenté el ciclo de carga: puse comprobaciones de pll_current_language() a lo largo del proceso, en orden de ejecución, y observé dónde se torcía el valor. Se mantenía correcto hasta muy tarde en la petición, y solo estaba mal cuando lo leía el propio método de comprobación canónica de Polylang (PLL_Canonical::check_canonical_url). Eso señalaba a Polylang, y mi primera conclusión fue exactamente esa: un defecto del plugin. Era una explicación plausible, encajaba con un caso límite que otros habían documentado, y el parche que escribí sobre esa base funcionaba. Así que la di por buena.

Ese fue el error, y darme cuenta de él es la parte de la que más me alegro. Más tarde, al intentar documentar bien el bug, contrasté la explicación con mis propios ajustes y se vino abajo: el idioma por defecto del sitio era el inglés, no el español, justo lo contrario de lo que mi teoría exigía. El parche funcionaba, pero la razón que le había puesto era falsa. Así que empecé de cero, y esta vez descarté con pruebas en lugar de razonar. Cookies y cabecera Accept-Language, comprobadas una a una con curl: la redirección saltaba pasara lo que pasara. El idioma del post más reciente, probado re-datando un post en inglés para que fuera el más nuevo del sitio: sin cambios. Después leí el código fuente de Polylang y vi que su comprobación canónica solo lee el idioma actual, nunca lo escribe, lo que significaba que era un testigo de la corrupción, no su causa. El valor ya estaba mal antes de que Polylang lo mirara.

Lo que lo zanjó fue apagarlo todo. Con todos mis snippets propios desactivados y Polylang a solas, el bug desaparecía. Así que nunca había sido Polylang. Una búsqueda binaria entre mis snippets, reactivándolos por mitades y volviendo a probar, dio con el que alimenta las tarjetas de artículo destacado de la portada, que había añadido al sitio dos semanas después del arreglo original.

La causa real estaba en mi propio código. Ese snippet construía la consulta de la portada casando las dos categorías de idioma a la vez, español e inglés juntas, en una única consulta ambigua. La comprobación canónica de Polylang lee esa consulta para decidir de qué idioma es la página, y ante dos categorías toma la primera de la lista, que resultaba ser la española, y concluye que la página es española siempre, independientemente de la URL. Por eso todas las pruebas sobre cookies y cabeceras habían dado negativo: ninguna de esas señales llegaba a leerse. Lo confirmé de la forma más directa posible, invirtiendo el orden de las dos categorías, y el bug se invirtió con él: ahora el español redirigía al inglés. Poder darle la vuelta a un bug cambiando una sola línea es lo más cerca de una prueba que se puede estar.

El arreglo fue pedir una sola categoría, la que corresponde al idioma que de verdad se está viendo, lo que elimina la ambigüedad que Polylang resolvía mal. Mi parche original, el que escribí cuando aún culpaba al plugin, cancela la redirección en la portada y restaura la bandera de idioma si alguna vez se corrompe. Ya no es imprescindible, pero lo mantengo activo como red de seguridad barata, porque se neutraliza solo una vez que la causa real está corregida.

Lo que me llevo de esto no es el arreglo, sino la corrección. El parche funcionaba, así que habría sido fácil dejar la explicación como estaba y seguir adelante. Lo que estaba mal era la historia, una plausible que nunca había contrastado con mi propia configuración, y el bug resultó ser mío, no del plugin. La única forma de descubrirlo era desconfiar de una explicación que ya funcionaba y seguir cavando. Ese hábito, mucho más que cualquier línea específica de PHP, es de lo que va de verdad encontrar soluciones ingenieriles.