Sincronizar mu-plugins/ y scripts/ con el estado real del sitio (2026-07-16)
Este repo llevaba desde el 28-jun sin actualizarse (salvo sync_carta_from_prod.py).
Se pone al dia con 3 semanas de trabajo que solo vivian en el checkout local
completo del proyecto:
- Fix del buscador (issue #178): AND obligatorio en FULLTEXT (antes devolvia
practicamente todo el sitio con cualquier busqueda), exclusion de paginas
indice residuales de la migracion K2, soporte de frase exacta entre comillas.
- Nuevos mu-plugins ya desplegados a prod: fea-carta-id-api, fea-cloudflare-realip,
fea-crear-autor-api, fea-gsc-verification, fea-legacy-redirect (cutover
Joomla->WP, ver issue #162).
- TTS multi-voz por autor, scripts de traduccion, sync de audio a prod,
homenajes Mardones/Galarreta.
Se mantiene la estructura plana del repo (mu-plugins/ + scripts/, sin el
wordpress/wp-content/ del checkout completo) tal y como documenta el README:
es la convencion establecida para este repo, mas limpia para quien solo
necesita leer el codigo. Anadido .gitignore (cache de Python que no debia
commitearse).
Fuente: checkout local completo del proyecto, rama main (commit 69e849d).
This commit is contained in:
@@ -9,14 +9,17 @@ if (!defined('ABSPATH')) exit;
|
||||
|
||||
/** Devuelve el HTML del reproductor para el post actual, o '' si no hay audio. */
|
||||
function fea_audio_player_html(): string {
|
||||
$url = get_post_meta(get_the_ID(), 'fea_audio_url', true);
|
||||
$id = get_the_ID();
|
||||
$url = get_post_meta($id, 'fea_audio_url', true);
|
||||
if (!$url) return '';
|
||||
$voice = get_post_meta($id, 'fea_audio_voice', true) ?: 'NicoFeadulta2026';
|
||||
return '<div class="fea-audio">'
|
||||
. '<span class="fea-audio-label">'
|
||||
. '<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false">'
|
||||
. '<path fill="currentColor" d="M3 10v4h4l5 5V5L7 10H3zm13.5 2a4.5 4.5 0 0 0-2.5-4.03v8.06A4.5 4.5 0 0 0 16.5 12zM14 3.23v2.06a7 7 0 0 1 0 13.42v2.06a9 9 0 0 0 0-17.54z"/>'
|
||||
. '</svg> Escucha</span>'
|
||||
. '<audio controls preload="none" src="' . esc_url($url) . '"></audio>'
|
||||
. '<audio controls preload="none" src="' . esc_url($url) . '"'
|
||||
. ' data-fea-audio-track data-post-id="' . esc_attr($id) . '" data-voice="' . esc_attr($voice) . '"></audio>'
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
@@ -60,3 +63,27 @@ add_action('wp_head', function () {
|
||||
</style>
|
||||
<?php
|
||||
});
|
||||
|
||||
// Evento GA4 audio_play (issue tracking uso TTS). Un único evento por <audio>
|
||||
// y carga de página, disparado en el primer 'play' (no en cada resume tras
|
||||
// pausa/seek). gtag ya está definido por fea-analytics.php en wp_head prio 1,
|
||||
// así que este script (footer) siempre lo encuentra disponible.
|
||||
add_action('wp_footer', function () {
|
||||
if (!is_singular('post')) return;
|
||||
if (!get_post_meta(get_queried_object_id(), 'fea_audio_url', true)) return;
|
||||
?>
|
||||
<script>
|
||||
document.querySelectorAll('audio[data-fea-audio-track]').forEach(function (audio) {
|
||||
var fired = false;
|
||||
audio.addEventListener('play', function () {
|
||||
if (fired || typeof gtag !== 'function') return;
|
||||
fired = true;
|
||||
gtag('event', 'audio_play', {
|
||||
post_id: audio.dataset.postId,
|
||||
voice: audio.dataset.voice
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
});
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — API carta_id
|
||||
* Description: Endpoint REST para que Inma y su asistente asignen el meta interno _carta_id.
|
||||
* Version: 1.0
|
||||
*/
|
||||
|
||||
add_action('rest_api_init', function() {
|
||||
register_rest_route('fea/v1', '/carta-id/(?P<post_id>\d+)', [
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => 'fea_carta_id_api_get',
|
||||
'permission_callback' => 'fea_carta_id_api_can_edit_post',
|
||||
'args' => fea_carta_id_api_route_args(),
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => 'fea_carta_id_api_update',
|
||||
'permission_callback' => 'fea_carta_id_api_can_edit_post',
|
||||
'args' => array_merge(fea_carta_id_api_route_args(), [
|
||||
'carta_id' => [
|
||||
'required' => true,
|
||||
],
|
||||
]),
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => 'fea_carta_id_api_delete',
|
||||
'permission_callback' => 'fea_carta_id_api_can_edit_post',
|
||||
'args' => fea_carta_id_api_route_args(),
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
function fea_carta_id_api_route_args() {
|
||||
return [
|
||||
'post_id' => [
|
||||
'required' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function fea_carta_id_api_can_edit_post(WP_REST_Request $request) {
|
||||
if (!is_user_logged_in()) {
|
||||
return new WP_Error(
|
||||
'fea_carta_id_not_authenticated',
|
||||
'Debes autenticarte para leer o modificar _carta_id.',
|
||||
['status' => 401]
|
||||
);
|
||||
}
|
||||
|
||||
$post_id = (int) $request['post_id'];
|
||||
if (!fea_carta_id_api_post_exists($post_id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!current_user_can('edit_post', $post_id)) {
|
||||
return new WP_Error(
|
||||
'fea_carta_id_forbidden',
|
||||
'No tienes permiso para editar este post.',
|
||||
['status' => 403]
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function fea_carta_id_api_get(WP_REST_Request $request) {
|
||||
$post_id = (int) $request['post_id'];
|
||||
$error = fea_carta_id_api_validate_post_id($post_id);
|
||||
if ($error) return $error;
|
||||
|
||||
return fea_carta_id_api_response($post_id);
|
||||
}
|
||||
|
||||
function fea_carta_id_api_update(WP_REST_Request $request) {
|
||||
$post_id = (int) $request['post_id'];
|
||||
$error = fea_carta_id_api_validate_post_id($post_id);
|
||||
if ($error) return $error;
|
||||
|
||||
$carta_id = fea_carta_id_api_parse_positive_int($request->get_param('carta_id'));
|
||||
if (!$carta_id) {
|
||||
return new WP_Error(
|
||||
'fea_carta_id_invalid',
|
||||
'carta_id debe ser un entero positivo.',
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
if (!fea_carta_id_api_post_exists($carta_id)) {
|
||||
return new WP_Error(
|
||||
'fea_carta_id_not_found',
|
||||
'carta_id debe corresponder a un post existente.',
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
update_post_meta($post_id, '_carta_id', $carta_id);
|
||||
|
||||
return fea_carta_id_api_response($post_id);
|
||||
}
|
||||
|
||||
function fea_carta_id_api_delete(WP_REST_Request $request) {
|
||||
$post_id = (int) $request['post_id'];
|
||||
$error = fea_carta_id_api_validate_post_id($post_id);
|
||||
if ($error) return $error;
|
||||
|
||||
delete_post_meta($post_id, '_carta_id');
|
||||
|
||||
return fea_carta_id_api_response($post_id);
|
||||
}
|
||||
|
||||
function fea_carta_id_api_validate_post_id($post_id) {
|
||||
if (!fea_carta_id_api_post_exists($post_id)) {
|
||||
return new WP_Error(
|
||||
'fea_carta_id_post_not_found',
|
||||
'post_id debe corresponder a un post existente.',
|
||||
['status' => 404]
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fea_carta_id_api_post_exists($post_id) {
|
||||
$post = get_post($post_id);
|
||||
return $post && $post->post_type === 'post';
|
||||
}
|
||||
|
||||
function fea_carta_id_api_parse_positive_int($value) {
|
||||
$int = filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
|
||||
return $int === false ? null : (int) $int;
|
||||
}
|
||||
|
||||
function fea_carta_id_api_response($post_id) {
|
||||
$carta_id = fea_carta_id_api_parse_positive_int(get_post_meta($post_id, '_carta_id', true));
|
||||
|
||||
return rest_ensure_response([
|
||||
'post_id' => (int) $post_id,
|
||||
'carta_id' => $carta_id,
|
||||
]);
|
||||
}
|
||||
@@ -140,9 +140,21 @@ function fea_url_to_post_id($url) {
|
||||
// Enlace interno WP: deriva el slug del path, relativo al home. Agnóstico al
|
||||
// entorno → funciona en local (home en .../fea) y en prod (home en la raíz).
|
||||
// No depende de un prefijo /fea/ hardcodeado (issue #91).
|
||||
$host = wp_parse_url($url, PHP_URL_HOST);
|
||||
$home_host = wp_parse_url(home_url(), PHP_URL_HOST);
|
||||
if ($host && $home_host && strcasecmp($host, $home_host) !== 0) {
|
||||
//
|
||||
// Durante el cutover de dominio (issue #158) el `home_url()` de WP sigue en
|
||||
// wp-nuevo.feadulta.com pero el contenido ya enlaza a www.feadulta.com. Para
|
||||
// no romper la resolución mientras el cutover no esté cerrado, se acepta
|
||||
// también una lista fija de hosts propios del sitio, no solo home_url().
|
||||
// TODO: cuando #158 quede resuelto y home_url() sea www.feadulta.com, esta
|
||||
// lista se puede simplificar a solo home_url().
|
||||
$own_hosts = array_filter(array_unique([
|
||||
strtolower((string) wp_parse_url(home_url(), PHP_URL_HOST)),
|
||||
'www.feadulta.com',
|
||||
'feadulta.com',
|
||||
'wp-nuevo.feadulta.com',
|
||||
]));
|
||||
$host = wp_parse_url($url, PHP_URL_HOST);
|
||||
if ($host && !in_array(strtolower($host), $own_hosts, true)) {
|
||||
return null; // host externo → no es un artículo nuestro
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: FEA Cloudflare Real IP
|
||||
* Description: Restaura la IP real del visitante en REMOTE_ADDR cuando la conexión
|
||||
* proviene de un rango oficial de Cloudflare, leyendo CF-Connecting-IP.
|
||||
* Necesario para que plugins de seguridad (LLAR), analytics y comentarios
|
||||
* vean la IP del usuario y no la del proxy de Cloudflare.
|
||||
* Auto-correctivo: si la conexión NO viene de Cloudflare, no toca nada.
|
||||
* Version: 1.0
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Rangos oficiales de Cloudflare (https://www.cloudflare.com/ips/).
|
||||
$fea_cf_ipv4 = array(
|
||||
'173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22',
|
||||
'141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20',
|
||||
'197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13',
|
||||
'104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22',
|
||||
);
|
||||
$fea_cf_ipv6 = array(
|
||||
'2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32',
|
||||
'2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32',
|
||||
);
|
||||
|
||||
if ( empty( $_SERVER['REMOTE_ADDR'] ) || empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fea_remote = $_SERVER['REMOTE_ADDR'];
|
||||
$fea_client = trim( $_SERVER['HTTP_CF_CONNECTING_IP'] );
|
||||
|
||||
/**
|
||||
* Comprueba si una IP está dentro de un CIDR (IPv4 o IPv6).
|
||||
*/
|
||||
function fea_ip_in_cidr( $ip, $cidr ) {
|
||||
list( $subnet, $bits ) = array_pad( explode( '/', $cidr, 2 ), 2, null );
|
||||
if ( null === $bits ) {
|
||||
return false;
|
||||
}
|
||||
$bits = (int) $bits;
|
||||
$ip_bin = @inet_pton( $ip );
|
||||
$subnet_bin = @inet_pton( $subnet );
|
||||
if ( false === $ip_bin || false === $subnet_bin || strlen( $ip_bin ) !== strlen( $subnet_bin ) ) {
|
||||
return false;
|
||||
}
|
||||
$bytes = intdiv( $bits, 8 );
|
||||
$rem = $bits % 8;
|
||||
if ( $bytes > 0 && 0 !== substr_compare( $ip_bin, $subnet_bin, 0, $bytes ) ) {
|
||||
return false;
|
||||
}
|
||||
if ( $rem > 0 ) {
|
||||
$mask = ~( ( 1 << ( 8 - $rem ) ) - 1 ) & 0xff;
|
||||
if ( ( ord( $ip_bin[ $bytes ] ) & $mask ) !== ( ord( $subnet_bin[ $bytes ] ) & $mask ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$fea_ranges = ( false !== strpos( $fea_remote, ':' ) ) ? $fea_cf_ipv6 : $fea_cf_ipv4;
|
||||
$fea_from_cf = false;
|
||||
foreach ( $fea_ranges as $fea_cidr ) {
|
||||
if ( fea_ip_in_cidr( $fea_remote, $fea_cidr ) ) {
|
||||
$fea_from_cf = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Solo confiamos en CF-Connecting-IP si la conexión proviene de Cloudflare,
|
||||
// y solo si el valor es una IP válida (evita inyecciones).
|
||||
if ( $fea_from_cf && false !== filter_var( $fea_client, FILTER_VALIDATE_IP ) ) {
|
||||
$_SERVER['REMOTE_ADDR'] = $fea_client;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fe Adulta — API crear-autor
|
||||
* Description: Endpoint REST acotado para que Mixbot (Inma) dé de alta autores
|
||||
* nuevos sin depender de que Rafa esté disponible, sin necesitar
|
||||
* el permiso sensible create_users de WP. El rol siempre es
|
||||
* 'author' (fijo en el código, no lo decide quien llama).
|
||||
* Idempotente: si el slug ya existe, devuelve el usuario existente.
|
||||
* Version: 1.0
|
||||
*
|
||||
* Ver issue gitea.feadulta.com/rafa/feadulta#166.
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
add_action('rest_api_init', function () {
|
||||
register_rest_route('fea/v1', '/crear-autor', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => 'fea_crear_autor_handle',
|
||||
'permission_callback' => 'fea_crear_autor_can_call',
|
||||
'args' => [
|
||||
'nombre' => ['required' => true],
|
||||
'slug' => ['required' => false],
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
/** Editor o superior (mismo nivel que ya usan Inma/Mixbot para editar posts). */
|
||||
function fea_crear_autor_can_call(WP_REST_Request $request) {
|
||||
if (!is_user_logged_in()) {
|
||||
return new WP_Error(
|
||||
'fea_crear_autor_not_authenticated',
|
||||
'Debes autenticarte para crear un autor.',
|
||||
['status' => 401]
|
||||
);
|
||||
}
|
||||
if (!current_user_can('edit_others_posts')) {
|
||||
return new WP_Error(
|
||||
'fea_crear_autor_forbidden',
|
||||
'No tienes permiso para crear autores.',
|
||||
['status' => 403]
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function fea_crear_autor_handle(WP_REST_Request $request) {
|
||||
$nombre = trim((string) $request->get_param('nombre'));
|
||||
if ($nombre === '') {
|
||||
return new WP_Error(
|
||||
'fea_crear_autor_invalid',
|
||||
'nombre es obligatorio.',
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
$slug_input = trim((string) $request->get_param('slug'));
|
||||
$login = sanitize_user(sanitize_title($slug_input !== '' ? $slug_input : $nombre), true);
|
||||
if ($login === '') {
|
||||
return new WP_Error(
|
||||
'fea_crear_autor_invalid',
|
||||
'No se pudo derivar un slug válido de nombre/slug.',
|
||||
['status' => 400]
|
||||
);
|
||||
}
|
||||
|
||||
// Idempotencia: si el login/slug ya existe, se devuelve tal cual (created=false).
|
||||
$existing = get_user_by('login', $login);
|
||||
if ($existing) {
|
||||
return fea_crear_autor_response($existing, false);
|
||||
}
|
||||
|
||||
$email = $login . '@feadulta.com';
|
||||
$i = 2;
|
||||
while (email_exists($email)) {
|
||||
$email = $login . $i . '@feadulta.com';
|
||||
$i++;
|
||||
}
|
||||
|
||||
$user_id = wp_insert_user([
|
||||
'user_login' => $login,
|
||||
'user_pass' => wp_generate_password(20),
|
||||
'user_email' => $email,
|
||||
'display_name' => $nombre,
|
||||
'nickname' => $nombre,
|
||||
'role' => 'author', // fijo: este endpoint nunca crea otro rol.
|
||||
]);
|
||||
|
||||
if (is_wp_error($user_id)) {
|
||||
return new WP_Error(
|
||||
'fea_crear_autor_failed',
|
||||
'No se pudo crear el usuario: ' . $user_id->get_error_message(),
|
||||
['status' => 500]
|
||||
);
|
||||
}
|
||||
|
||||
return fea_crear_autor_response(get_userdata($user_id), true);
|
||||
}
|
||||
|
||||
function fea_crear_autor_response(WP_User $user, bool $created) {
|
||||
return rest_ensure_response([
|
||||
'id' => $user->ID,
|
||||
'login' => $user->user_login,
|
||||
'slug' => $user->user_nicename,
|
||||
'display_name' => $user->display_name,
|
||||
'role' => 'author',
|
||||
'created' => $created,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Fea Search Console Verification
|
||||
* Description: Mantiene la verificación de Google Search Console para
|
||||
* www.feadulta.com tras el cutover Joomla -> WordPress (la plantilla de
|
||||
* Joomla la tenía hardcodeada; en WP no había ningún sitio equivalente).
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
add_action('wp_head', function () {
|
||||
if (is_admin()) return;
|
||||
echo '<meta name="google-site-verification" content="wZ_edk-78QW3w8aXtiYpzTPEJ8_dh8MkhaIDSTq8U-8" />' . "\n";
|
||||
}, 1);
|
||||
@@ -790,9 +790,40 @@ function fea_title(string $title): string {
|
||||
$out = preg_replace_callback('/([\/:¿¡] *)(\p{Ll})/u', function ($m) {
|
||||
return $m[1] . mb_strtoupper($m[2], 'UTF-8');
|
||||
}, $out);
|
||||
$out = fea_recapitalizar_nombres_propios($out);
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nombres propios (personas, lugares, "Dios"/"Papa") que deben mantener mayúscula
|
||||
* inicial en los títulos de portada. Los títulos originales se guardan en MAYÚSCULAS
|
||||
* y fea_title() los minusculiza salvo la primera letra, así que esta señal se pierde
|
||||
* para cualquier nombre propio que no sea la primera palabra del título. Añadir aquí
|
||||
* cuando una carta nueva mencione un nombre que salga mal capitalizado en portada.
|
||||
* Ver issue rafa/feadulta#160.
|
||||
*/
|
||||
function fea_nombres_propios(): array {
|
||||
return [
|
||||
'Aguirre', 'Crepin', 'Monga', 'Papa', 'Lampedusa', 'Europa', 'España', 'Dios',
|
||||
];
|
||||
}
|
||||
|
||||
function fea_recapitalizar_nombres_propios(string $texto): string {
|
||||
static $regex = null, $map = null;
|
||||
if ($regex === null) {
|
||||
$nombres = fea_nombres_propios();
|
||||
usort($nombres, fn($a, $b) => mb_strlen($b, 'UTF-8') - mb_strlen($a, 'UTF-8'));
|
||||
$alt = implode('|', array_map(fn($n) => preg_quote($n, '/'), $nombres));
|
||||
$regex = '/(?<![\p{L}])(' . $alt . ')(?![\p{L}])/iu';
|
||||
$map = [];
|
||||
foreach ($nombres as $n) $map[mb_strtolower($n, 'UTF-8')] = $n;
|
||||
}
|
||||
return preg_replace_callback($regex, function ($m) use ($map) {
|
||||
$k = mb_strtolower($m[1], 'UTF-8');
|
||||
return $map[$k] ?? $m[1];
|
||||
}, $texto);
|
||||
}
|
||||
|
||||
/** Lista de libros bíblicos (para avatar genérico de lecturas/eucaristías). #61 */
|
||||
function fea_libros_biblicos(): array {
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* URLs de Joomla que no tengan equivalente en WordPress (404 genuino) se
|
||||
* redirigen a antiguo.feadulta.com, donde Joomla se sigue sirviendo tras el
|
||||
* cutover, en vez de mostrar el 404 de WordPress.
|
||||
*/
|
||||
|
||||
add_action('template_redirect', function () {
|
||||
if (!is_404()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = untrailingslashit(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
|
||||
|
||||
// La antigua home de Joomla (con prefijo de idioma /es/) debe llevar a
|
||||
// la home nueva, no a la web antigua.
|
||||
if ($path === '/es') {
|
||||
wp_redirect(home_url('/'), 301);
|
||||
exit;
|
||||
}
|
||||
|
||||
wp_redirect('https://antiguo.feadulta.com' . $_SERVER['REQUEST_URI'], 301);
|
||||
exit;
|
||||
}, 0);
|
||||
@@ -1,23 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* fea-pensamientos — Galerías Joomla y pausa aleatoria en artículos.
|
||||
* fea-pensamientos — Galerías (Pensamientos, jornadas) y pausa aleatoria en artículos.
|
||||
*
|
||||
* Reutiliza /images de Joomla sin duplicar ficheros en WordPress.
|
||||
* Las imágenes viven en wp-content/uploads/joomla-galleries/ (copiadas de Joomla
|
||||
* el 2026-07-08 tras el cutover) — WordPress ya no depende del filesystem de
|
||||
* Joomla/antiguo.feadulta.com para servirlas.
|
||||
*/
|
||||
|
||||
if (!defined('FEA_JOOMLA_IMAGES_DIR')) {
|
||||
define('FEA_JOOMLA_IMAGES_DIR', file_exists('/web/images') ? '/web/images' : '/var/www/joomla-images');
|
||||
define('FEA_JOOMLA_IMAGES_DIR', WP_CONTENT_DIR . '/uploads/joomla-galleries');
|
||||
}
|
||||
|
||||
if (!defined('FEA_JOOMLA_IMAGES_URL')) {
|
||||
$fea_is_prod = (defined('ABSPATH') && strpos((string) ABSPATH, '/web/wp-nuevo/') === 0)
|
||||
|| (isset($_SERVER['HTTP_HOST']) && preg_match('/(^|\.)feadulta\.com$/', (string) $_SERVER['HTTP_HOST']))
|
||||
|| file_exists('/web/images');
|
||||
|
||||
define(
|
||||
'FEA_JOOMLA_IMAGES_URL',
|
||||
$fea_is_prod ? 'https://www.feadulta.com/images' : 'https://farmer.taild3aaf6.ts.net/joomla/images'
|
||||
);
|
||||
define('FEA_JOOMLA_IMAGES_URL', content_url('uploads/joomla-galleries'));
|
||||
}
|
||||
|
||||
if (!defined('FEA_PENS_DIR')) {
|
||||
|
||||
@@ -32,6 +32,16 @@ defined('FEA_AUTORES_EXCLUIR') or define('FEA_AUTORES_EXCLUIR', [
|
||||
*/
|
||||
defined('FEA_CATS_CARTA_EXCLUIR') or define('FEA_CATS_CARTA_EXCLUIR', [6, 21, 22]);
|
||||
|
||||
/**
|
||||
* Categorías RESIDUALES de la migración K2: páginas-índice por autor (listado de enlaces
|
||||
* a sus artículos), no son artículos. 37 = "Lista de autores habituales", 38 = "Lista
|
||||
* completa de autores por orden alfabético" (~1.169 posts). Por su gran tamaño (varios KB
|
||||
* de títulos enlazados) el ranking FULLTEXT las puntúa altísimo y tapan el resultado real
|
||||
* (bug reportado en feedback: "Tu verdadero ser" de Fray Marcos sacaba la ficha-índice del
|
||||
* autor antes que el artículo). Se excluyen de los resultados de búsqueda.
|
||||
*/
|
||||
defined('FEA_CATS_RESIDUAL_EXCLUIR') or define('FEA_CATS_RESIDUAL_EXCLUIR', [37, 38]);
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// i18n mínimo (es / en / fr / it / pt)
|
||||
@@ -139,6 +149,12 @@ add_action('pre_get_posts', function (WP_Query $q): void {
|
||||
// Categoría (tema)
|
||||
if ($fea_cat > 0) $q->set('cat', $fea_cat);
|
||||
|
||||
// Excluir páginas-índice residuales K2 (ver FEA_CATS_RESIDUAL_EXCLUIR) de los
|
||||
// resultados de búsqueda. No aplica si el usuario ya filtró por una categoría concreta.
|
||||
if ($is_search && $fea_cat <= 0) {
|
||||
$q->set('category__not_in', FEA_CATS_RESIDUAL_EXCLUIR);
|
||||
}
|
||||
|
||||
// Cita bíblica: coincidencia por PREFIJO (el valor empieza por el término, ej. "Jn").
|
||||
// Usamos REGEXP '^<term>' con el término escapado para evitar metacaracteres.
|
||||
if ($fea_cita !== '') {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* relevancia FULLTEXT si no se pide otro criterio de orden. Degradación elegante:
|
||||
* si no hay término o el índice FULLTEXT no existe, usa el comportamiento nativo.
|
||||
* Convive con fea-search-advanced.php (filtros pre_get_posts de autor/cat/cita/fecha).
|
||||
* Version: 1.1
|
||||
* Version: 1.3
|
||||
*/
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
@@ -38,16 +38,46 @@ function fea_ft_index_exists(): bool {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula el término FULLTEXT en Boolean Mode (cada palabra con prefijo *).
|
||||
* Calcula el término FULLTEXT en Boolean Mode.
|
||||
*
|
||||
* Caso normal (sin comillas): cada palabra requerida con prefijo + y sufijo *. El '+'
|
||||
* fuerza AND entre palabras — sin él, Boolean Mode las une con OR implícito y cualquier
|
||||
* post que contenga UNA sola palabra común (p.ej. "ser") entra en el resultado, lo que en
|
||||
* la práctica devolvía casi todo el sitio (~24.700 de ~24.780 posts, bug #8 reportado en
|
||||
* feedback: "Tu verdadero ser" devolvía cientos de artículos irrelevantes).
|
||||
*
|
||||
* Caso frase exacta (entrecomillado, ej. `"tu verdadero ser"`): Boolean Mode soporta
|
||||
* nativamente búsqueda de frase entre comillas dobles (coincidencia de adyacencia, no
|
||||
* solo de palabras sueltas). Reenviamos el contenido saneado tal cual entre comillas, sin
|
||||
* partirlo en palabras con + ni *, para que el usuario que escribe entre comillas obtenga
|
||||
* de verdad una búsqueda de cadena exacta.
|
||||
*
|
||||
* Devuelve '' si el término sanitizado queda vacío.
|
||||
*/
|
||||
function fea_ft_boolean_term(string $raw): string {
|
||||
// Frase exacta: todo el término entre comillas dobles.
|
||||
if (preg_match('/^"(.*)"$/us', $raw, $m)) {
|
||||
$phrase = trim(substr(preg_replace('/[^\p{L}\p{N}\s\'\-]/u', '', $m[1]), 0, 200));
|
||||
if ($phrase === '') return '';
|
||||
return '"' . $phrase . '"';
|
||||
}
|
||||
|
||||
$term = trim(substr(preg_replace('/[^\p{L}\p{N}\s\'\-]/u', '', $raw), 0, 200));
|
||||
if ($term === '') return '';
|
||||
|
||||
global $wpdb;
|
||||
$words = preg_split('/\s+/', $term);
|
||||
return implode('* ', array_map(fn($w) => $wpdb->esc_like($w), $words)) . '*';
|
||||
|
||||
// Descartamos palabras por debajo de innodb_ft_min_token_size (3 en este servidor):
|
||||
// MySQL las excluye del índice, y forzarlas igualmente como término obligatorio ('+')
|
||||
// no aporta nada al filtrado (WP igual las ignora) pero SÍ rompe el cálculo de
|
||||
// relevancia: si un término '+' no existe en el índice, MATCH()...AGAINST() en modo
|
||||
// boolean puede devolver 0 aunque la fila cumpla el resto de términos, dejando el
|
||||
// ORDER BY sin poder distinguir resultados relevantes de irrelevantes.
|
||||
$words = array_values(array_filter($words, fn($w) => mb_strlen($w) >= 3));
|
||||
if (empty($words)) return '';
|
||||
|
||||
return implode(' ', array_map(fn($w) => '+' . $wpdb->esc_like($w) . '*', $words));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
/**
|
||||
* fea-share — Sección "Comparte Fe Adulta" en single posts + Open Graph tags.
|
||||
* Botones: Facebook, Instagram (Web Share API + fallback copiar), Imprimir.
|
||||
* Sin plugins externos, sin JS de terceros, sin tracking.
|
||||
* Sin plugins externos, sin JS de terceros. El botón de Facebook dispara un
|
||||
* evento GA4 share_click (mismo patrón que fea-audio-player.php: data-attribute
|
||||
* + listener en wp_footer que llama a gtag, ya inicializado por fea-analytics.php).
|
||||
*/
|
||||
|
||||
/** Solo artículos reales: single post_type=post, excluyendo institucionales. */
|
||||
@@ -44,6 +46,7 @@ function fea_share_block_html(): string {
|
||||
$svg_pr = '<svg viewBox="0 0 24 24" width="17" height="17" aria-hidden="true" focusable="false" fill="currentColor"><path d="M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7a1 1 0 0 1 0-2 1 1 0 0 1 0 2zm-1-9H6v4h12V3z"/></svg>';
|
||||
|
||||
$copied_msg = esc_js($t['copied']);
|
||||
$post_id = get_the_ID();
|
||||
|
||||
return '
|
||||
<div class="fea-share">
|
||||
@@ -52,7 +55,8 @@ function fea_share_block_html(): string {
|
||||
<span class="fea-share-label">' . esc_html($t['section']) . '</span>
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u=' . $url . '"
|
||||
target="_blank" rel="noopener noreferrer nofollow"
|
||||
class="fea-share-item fea-share-fb">
|
||||
class="fea-share-item fea-share-fb"
|
||||
data-fea-share-track="facebook" data-post-id="' . esc_attr($post_id) . '">
|
||||
' . $svg_fb . '<span>' . esc_html($t['fb']) . '</span>
|
||||
</a>
|
||||
<span class="fea-share-sep" aria-hidden="true">·</span>
|
||||
@@ -198,6 +202,15 @@ add_action('wp_footer', function () {
|
||||
}
|
||||
});
|
||||
})();
|
||||
document.querySelectorAll('[data-fea-share-track]').forEach(function (el) {
|
||||
el.addEventListener('click', function () {
|
||||
if (typeof gtag !== 'function') return;
|
||||
gtag('event', 'share_click', {
|
||||
method: el.dataset.feaShareTrack,
|
||||
post_id: el.dataset.postId
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
}, 20);
|
||||
|
||||
Reference in New Issue
Block a user