fea-homepage: fuera url_to_postid(), reventaba 36 cartas traducidas

36 URLs en 500 desde el cutover: las mismas 9 cartas de otras semanas en
fr/it/en/pt. En espanol iban bien, y contra CDMON las mismas URLs daban 200, o
sea que era regresion nuestra.

La reescritura de enlaces internos al idioma activo llamaba a url_to_postid()
una vez por enlace, y estas cartas traen unos 40. Cuando la URL no encaja en
ninguna regla de reescritura, la WP_Query que monta esa funcion se queda sin
clausula que la acote y se trae las 32.311 entradas CON su contenido, dos veces
por peticion. 256 MB agotados en class-wpdb.php y 500.

Se sustituye por fea_href_a_post_id(): ultimo segmento del path y una consulta
con LIMIT 1, que no puede degenerar, cacheada por peticion.

El ORDER BY reproduce a quien sirve WordPress esa misma URL, y no es cosmetico:
hay 4 slugs compartidos por una pagina de primer nivel y una entrada, donde gana
la pagina, y 58 compartidos por dos entradas -duplicados del import de Joomla-
donde gana la de post_date mas reciente. Mi primer intento ordenaba por ID ASC y
en esos 58 habria traducido el enlace equivocado.

Verificado: 116/116 cartas traducidas en 200, 200 entradas al azar en 200,
E2E 13/13, y el resolutor nuevo coincide con url_to_postid() en 461 de 461
slugs de los casos donde url_to_postid() no revienta. La consulta gorda ha
desaparecido del performance_schema.

Van dos centinelas a la suite E2E, una carta italiana y una francesa de las que
fallaban.

Nota de metodo para el futuro: esto no se reproduce con wp-cli, porque Polylang
no instancia su frontend en CLI y el filtro sale antes de tiempo. Se cazo
mirando events_statements_history_long en MySQL mientras se pedia la pagina por
HTTP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 22:02:10 -04:00
parent d98f99d0ab
commit 78879aef68
4 changed files with 144 additions and 8 deletions
@@ -0,0 +1,78 @@
# Los 500 de las cartas traducidas (`url_to_postid()`)
## ✅ RESUELTO — 4-ago-2026, 02:00 UTC
**36 URLs devolvían 500**: las mismas 9 «cartas de otras semanas» en los cuatro idiomas
traducidos. En español las 9 iban bien.
```
/fr/lumiere-et-phare/ /it/luce-e-faro/ /en/light-and-beacon/ /pt/luz-e-farol/ … ×9
```
Era **una regresión del cutover, no algo heredado**: las mismas URLs contra CDMON respondían 200.
## La causa
`fea-homepage.php` reescribe los enlaces internos al idioma activo, y para eso llamaba a
**`url_to_postid()`** una vez por enlace. Estas cartas traen unos 40.
`url_to_postid()` acaba construyendo una `WP_Query` a partir de las reglas de reescritura. Cuando
la URL no encaja en ninguna, la query se queda **sin cláusula que la acote** y sale esto:
```sql
SELECT wp_posts.* FROM wp_posts WHERE 1=1 AND wp_posts.post_type = 'post' ORDER BY post_date DESC
```
**Las 32.311 entradas con su contenido entero.** Dos veces por petición. Los 256 MB de PHP se
agotaban en `class-wpdb.php:2322` y Apache devolvía el 500.
Encaja con el patrón observado: el filtro se salta el español (`if (!$lang || $lang === 'es')
return`), que es justo el idioma que no fallaba, y solo actúa en `is_singular()`.
## El arreglo
Se sustituye `url_to_postid()` por `fea_href_a_post_id()`: la estructura de enlaces es
`/%postname%/`, así que basta el último segmento del path y **una consulta con `LIMIT 1`**, que no
puede degenerar por rara que sea la URL. El resultado se cachea por petición.
El orden del `ORDER BY` no es cosmético, reproduce a quién sirve WordPress esa misma URL:
| caso | cuántos | criterio |
|---|---|---|
| página de primer nivel con el mismo slug que una entrada | 4 | gana la **página** (reglas verbosas de reescritura) |
| dos entradas con el mismo slug (duplicados del import de Joomla) | 58 | gana la de **`post_date` más reciente** |
Ordenar por `ID ASC`, que fue el primer intento, devolvía la entrada vieja y habría traducido el
enlace equivocado en esos 58 casos.
## Verificación
| | |
|---|---|
| Las 116 cartas traducidas (29 × 4 idiomas) | **116/116 en 200** |
| Muestra de 200 entradas al azar (40 por idioma) | todas en 200 |
| `fea_href_a_post_id()` vs `url_to_postid()`, 61 slugs repetidos + 400 al azar | **461/461 idénticos** |
| La consulta gorda | desaparecida: la que más filas devuelve ahora son 640 de `wp_options` |
| Suite E2E | **13/13 en 200** |
| Errores PHP y 5xx tras el despliegue | 0 y 0 |
Los enlaces se siguen reescribiendo, y **algunos más que antes**: `url_to_postid()` fallaba en
URLs que sí resuelven bien por slug. Comprobado contra CDMON que los nuevos pares son de verdad la
misma entrada (mismo grupo de traducción de Polylang): `como-bendecir-la-mesa`
`comment-benir-la-table`, `felices-6``heureux`, `3-temario``programme`
## Centinelas
`tools/e2e/sites/www.json` incorpora `carta-trad-it` y `carta-trad-fr`, dos de las que reventaban.
Si esto vuelve, lo dice la suite.
## ⚠️ Para la próxima
**`url_to_postid()` no es seguro con URLs arbitrarias en un sitio grande.** El comentario que ya
había en el fichero decía que en los listados agotaba memoria, y por eso se había acotado a
`is_singular()`. La cura se quedó corta: el problema no era el listado, era la función.
Y una trampa de método: esto **no se reproduce con wp-cli**. Polylang no instancia su clase de
frontend en CLI, así que el filtro sale antes de tiempo y no pasa nada. Se cazó mirando
`performance_schema.events_statements_history_long` en MySQL mientras se pedía la página por HTTP,
que muestra la secuencia real de sentencias de la petición.
+1 -1
View File
@@ -22,7 +22,7 @@ d3d8bfaf176f14cf80dd4b94e0c65682cdc19ff2cfd6226d14fb07bd4914448e fea-cookie-con
eee49e2bc543f6bf03ce6083ee276a968532e38b915373317f15b49629c805e5 fea-disable-comments.php
721c52fdeab85e6bbc512b65792ca3a7d18c895d79d92c03714ff93136211a63 fea-gsc-verification.php
351734516531857c92cfec40c78faa5417866694e1c9e6976f1080ec29bbc77b fea-hide-bad-tag.php
7a15a508e2f3d9d94ed85141de96f07ce31a43298fae3cf09674fe9e7b031e35 fea-homepage.php
ccac15d46eae66f4782c7a859d22d6553c80be2324cd498309e6304e0a953455 fea-homepage.php
302da523f8c77ea2d2488fcd946d4e1207d49fbe39f26940115b7264aa268227 fea-legacy-redirect.php
ecced1dc780edaabce659047860092428904b9a79688e908227f7ce086aa86c6 fea-menu-i18n.php
cef207908964319efc0fb3a5929200f8fb9a4dbcce67e5b6bd2712b5cb83289b fea-pensamientos.php
+4 -2
View File
@@ -5,7 +5,7 @@
"timeoutMs": 45000,
"userAgent": "feadulta-e2e/0.1 (+local)",
"hostResolverRules": "MAP www.feadulta.com 188.40.120.157, MAP feadulta.com 188.40.120.157",
"notes": "Produccion despues del cutover del 3-ago. Se resuelve a mano contra el Hetzner porque el WAF de Cloudflare devuelve 403 a nuestras IPs: por el dominio real no se puede verificar nada. Asi se comprueba el origen, que es lo que hemos cambiado.",
"notes": "Produccion despues del cutover del 3-ago. Se resuelve a mano contra el Hetzner porque el WAF de Cloudflare devuelve 403 a nuestras IPs: por el dominio real no se puede verificar nada. Asi se comprueba el origen, que es lo que hemos cambiado. Las dos carta-trad-* son cartas traducidas que dieron 500 por el url_to_postid() de fea-homepage: quedan aqui de centinela.",
"urls": [
{ "slug": "home-es", "path": "/" },
{ "slug": "home-en", "path": "/en/" },
@@ -17,6 +17,8 @@
{ "slug": "evangelio-dia", "path": "/evangelio-de-cada-dia/" },
{ "slug": "autores", "path": "/autores-lista/" },
{ "slug": "buscador", "path": "/?s=evangelio" },
{ "slug": "wp-login", "path": "/wp-login.php" }
{ "slug": "wp-login", "path": "/wp-login.php" },
{ "slug": "carta-trad-it", "path": "/it/luce-e-faro/" },
{ "slug": "carta-trad-fr", "path": "/fr/pasteurs-et-petits-bergers/" }
]
}
+61 -5
View File
@@ -2,7 +2,7 @@
/**
* Plugin Name: Fe Adulta — Homepage
* Description: Portada con selección editorial via ACF.
* Version: 1.4
* Version: 1.5
*/
// ── Flush rewrite rules una sola vez tras cambios de configuración ────────
@@ -1407,13 +1407,69 @@ add_shortcode('fea_noticia_centro', function() {
});
// ── Reescribir links internos al idioma activo (Polylang) ─────────────────
/**
* Resuelve un enlace interno a un ID de entrada, con coste acotado.
*
* NO usa url_to_postid(). Esa función termina construyendo una WP_Query a partir
* de las reglas de reescritura, y cuando la URL no encaja en ninguna (enlaces
* legacy de Joomla, rutas de idioma, .html sueltos) la query se queda sin cláusula
* que la acote y se trae las ~32.000 entradas CON su contenido: 256 MB de memoria
* y un 500. Reventaba 36 URLs —9 cartas × los 4 idiomas traducidos— desde el
* cutover del 3-ago.
*
* La estructura de enlaces es /%postname%/, así que basta con el último segmento
* del path. Una consulta con LIMIT 1 no puede degenerar por mucho que la URL sea
* rara, y el resultado se cachea por petición: las cartas traen ~40 enlaces y
* muchos se repiten.
*/
function fea_href_a_post_id(string $href): int {
static $cache = [];
if (isset($cache[$href])) return $cache[$href];
$path = (string) parse_url($href, PHP_URL_PATH);
$segmentos = array_values(array_filter(explode('/', $path), 'strlen'));
if (!$segmentos) return $cache[$href] = 0;
$ultimo = (string) end($segmentos);
// Las URLs viejas de Joomla acaban en .html y nunca son un post_name.
if (substr($ultimo, -5) === '.html') return $cache[$href] = 0;
// El slug puede venir percent-encoded en el href (introducci%C3%B3n-...).
$candidatos = array_values(array_unique(array_filter([
$ultimo,
rawurldecode($ultimo),
sanitize_title(rawurldecode($ultimo)),
], 'strlen')));
if (!$candidatos) return $cache[$href] = 0;
// El orden reproduce a quién sirve WordPress esa misma URL:
// - con la estructura /%postname%/ las páginas de primer nivel ganan al post
// homónimo (reglas verbosas de reescritura); hay 4 slugs así.
// - hay 58 slugs compartidos por dos entradas —duplicados del import de
// Joomla— y WP resuelve el name por post_date DESC. Ordenar por ID daría
// la vieja y traduciríamos el enlace equivocado.
global $wpdb;
$marcas = implode(',', array_fill(0, count($candidatos), '%s'));
$id = (int) $wpdb->get_var($wpdb->prepare(
"SELECT ID FROM {$wpdb->posts}
WHERE post_name IN ($marcas)
AND post_status = 'publish'
AND post_type IN ('post','page')
ORDER BY (post_type = 'page' AND post_parent = 0) DESC, post_date DESC, ID DESC
LIMIT 1",
$candidatos
));
return $cache[$href] = $id;
}
add_filter('the_content', function($content) {
if (!function_exists('pll_current_language') || !function_exists('pll_get_post')) return $content;
$lang = pll_current_language();
if (!$lang || $lang === 'es') return $content;
// Los archivos de cartas pueden contener cientos de enlaces. Resolver cada uno
// con url_to_postid() en un listado agota memoria; la reescritura solo aporta
// valor al mostrar el contenido completo de una entrada o página.
// La reescritura solo aporta valor al mostrar el contenido completo de una
// entrada o página, no en los listados.
if (!is_singular()) return $content;
return preg_replace_callback(
@@ -1423,7 +1479,7 @@ add_filter('the_content', function($content) {
$home = home_url();
if (strpos($href, $home) === false) return $m[0];
$post_id = url_to_postid($href);
$post_id = fea_href_a_post_id($href);
if (!$post_id) return $m[0];
$translated_id = pll_get_post($post_id, $lang);