From 382a6a6e879d4ded6418bf5c1c8716e7ae66c119 Mon Sep 17 00:00:00 2001 From: rafa Date: Mon, 25 May 2026 12:26:01 -0400 Subject: [PATCH] =?UTF-8?q?feat(portada):=20modelo=20carta=E2=86=92portada?= =?UTF-8?q?=20+=20slider=20sync=20desde=20filesystem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fea-carta-portada.php (nuevo, 1.0): parser de la carta semanal. Extrae secciones (Evangelio, Artículos, Eucaristía, Multimedia, EFFA) por encabezados y resuelve los href (WP slug o K2 legacy id) a wp_posts.ID. Cache en transient 15 min, invalidación en save_post. Cierra issue #38. - fea-homepage.php: 4 shortcodes reescritos para usar fea_carta_section_posts() como fuente primaria, manteniendo el fallback existente (ACF + últimos por categoría). Afecta: [fea_articulos_semana], [fea_evangelio], [fea_eucaristia], [fea_multimedia]. El hero y noticia_centro sin cambios. - fea-slider-sync.php (nuevo, 1.0): sincroniza Smart Slider 3 id 2 con uploads/home/. Editor sube/borra imágenes en esa carpeta y el slider se actualiza automáticamente al primer pageview (modelo paridad con Joomla mod_ariimageslider). Cierra issue #43. Co-Authored-By: Claude Opus 4.7 --- .../mu-plugins/fea-carta-portada.php | 184 +++++++++++++++++ .../wp-content/mu-plugins/fea-homepage.php | 163 ++++++++++++--- .../wp-content/mu-plugins/fea-slider-sync.php | 185 ++++++++++++++++++ 3 files changed, 501 insertions(+), 31 deletions(-) create mode 100644 wordpress/wp-content/mu-plugins/fea-carta-portada.php mode change 100644 => 100755 wordpress/wp-content/mu-plugins/fea-homepage.php create mode 100644 wordpress/wp-content/mu-plugins/fea-slider-sync.php diff --git a/wordpress/wp-content/mu-plugins/fea-carta-portada.php b/wordpress/wp-content/mu-plugins/fea-carta-portada.php new file mode 100644 index 0000000..c5461fc --- /dev/null +++ b/wordpress/wp-content/mu-plugins/fea-carta-portada.php @@ -0,0 +1,184 @@ + 1, + 'category__in' => [$cat], + 'post_status' => 'publish', + 'orderby' => 'date', + 'order' => 'DESC', + 'suppress_filters' => false, + ]); + if (!$cartas) return $cache[$lang] = 0; + + $carta_id = (int) $cartas[0]->ID; + + if ($lang !== 'es' && function_exists('pll_get_post')) { + $trans = pll_get_post($carta_id, $lang); + if ($trans) $carta_id = (int) $trans; + } + + return $cache[$lang] = $carta_id; +} + +/** + * Parsea el HTML de la carta y devuelve los post_ids agrupados por sección. + */ +function fea_parse_carta_sections($carta_id) { + static $mem = []; + $carta_id = (int) $carta_id; + if (!$carta_id) return []; + if (isset($mem[$carta_id])) return $mem[$carta_id]; + + $tk = 'fea_carta_sections_' . $carta_id; + $cached = get_transient($tk); + if (is_array($cached)) return $mem[$carta_id] = $cached; + + $post = get_post($carta_id); + if (!$post) return $mem[$carta_id] = []; + + $sections = fea_extract_sections_from_html($post->post_content); + + set_transient($tk, $sections, 15 * MINUTE_IN_SECONDS); + return $mem[$carta_id] = $sections; +} + +/** + * Extrae secciones del HTML. Pública para tests/CLI. + */ +function fea_extract_sections_from_html($html) { + $section_patterns = [ + 'evangelio' => '/Evangelio\s+y\s+comentarios\s+al\s+Evangelio/iu', + 'articulos' => '/Art[ií]culos\s+seleccionados\s+para\s+la\s+semana/iu', + 'eucaristia' => '/Para\s+unas\s+eucarist[ií]as\s+m[áa]s\s+participativas/iu', + 'multimedia' => '/Material\s+multimedia/iu', + 'effa' => '/Escuela\s+EFFA/iu', + ]; + + $positions = []; + foreach ($section_patterns as $slug => $regex) { + if (preg_match($regex, $html, $m, PREG_OFFSET_CAPTURE)) { + $positions[$slug] = $m[0][1]; + } + } + if (empty($positions)) return []; + asort($positions); + + $slugs = array_keys($positions); + $offsets = array_values($positions); + $offsets[] = strlen($html); + + $sections = []; + for ($i = 0; $i < count($slugs); $i++) { + $segment = substr($html, $offsets[$i], $offsets[$i+1] - $offsets[$i]); + $ids = fea_resolve_links_in_html($segment); + if ($ids) $sections[$slugs[$i]] = $ids; + } + return $sections; +} + +/** + * Extrae los href de un fragmento HTML y los resuelve a wp_posts.ID. + */ +function fea_resolve_links_in_html($html) { + if (!preg_match_all('/href=["\']([^"\']+)["\']/i', $html, $m)) return []; + $ids = []; + $seen = []; + foreach ($m[1] as $url) { + $pid = fea_url_to_post_id($url); + if ($pid && !isset($seen[$pid])) { + $seen[$pid] = true; + $ids[] = $pid; + } + } + return $ids; +} + +/** + * Resuelve una URL (WP o Joomla legacy) a wp_posts.ID o null. + */ +function fea_url_to_post_id($url) { + global $wpdb; + + if (preg_match('~(?:^|/)fea/([a-z0-9\-]+)/?(?:[?#]|$)~i', $url, $m)) { + $slug = $m[1]; + if (in_array($slug, ['wp-admin','wp-content','category','tag','author','page','en','fr','it','pt'], true)) { + return null; + } + $pid = $wpdb->get_var($wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} + WHERE post_name=%s AND post_status='publish' AND post_type='post' + ORDER BY post_date DESC LIMIT 1", + $slug + )); + if ($pid) return (int) $pid; + } + + if (preg_match('~/item/(\d+)-[^/"]+\.html~i', $url, $m)) { + $k2 = (int) $m[1]; + if ($k2 > 0) { + $pid = $wpdb->get_var($wpdb->prepare( + "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key='_fgj2wp_old_k2_id' AND meta_value=%s LIMIT 1", + (string) $k2 + )); + if ($pid) return (int) $pid; + } + } + + return null; +} + +/** + * Devuelve los WP_Post objects de una sección de la carta vigente, + * o array vacío si no hay carta o no hay links resueltos en esa sección. + */ +function fea_carta_section_posts($section_slug, $lang = null) { + $carta_id = fea_get_current_carta_id($lang); + if (!$carta_id) return []; + $sections = fea_parse_carta_sections($carta_id); + $ids = $sections[$section_slug] ?? []; + if (!$ids) return []; + $posts = []; + foreach ($ids as $pid) { + $p = get_post($pid); + if ($p && $p->post_status === 'publish') $posts[] = $p; + } + return $posts; +} + +/** + * Invalida transients de secciones al guardar/editar un post. + */ +add_action('save_post_post', function($post_id, $post, $update) { + $cats = wp_get_post_categories($post_id); + $watch = [6, 21, 22]; + if (array_intersect($cats, $watch)) { + global $wpdb; + $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_fea_carta_sections_%'"); + $wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_fea_carta_sections_%'"); + } +}, 10, 3); diff --git a/wordpress/wp-content/mu-plugins/fea-homepage.php b/wordpress/wp-content/mu-plugins/fea-homepage.php old mode 100644 new mode 100755 index d09ec13..ede84a1 --- a/wordpress/wp-content/mu-plugins/fea-homepage.php +++ b/wordpress/wp-content/mu-plugins/fea-homepage.php @@ -437,6 +437,56 @@ add_action('wp_head', function() { echo ''; }, 20); +// ── H1 semántico oculto para páginas sin título visible ─────────────────── +add_action('wp_head', function() { + ?> + + ' . esc_html($title) . '' . $content; +}, 5); + +// ── Ocultar metadatos de post en páginas estáticas ──────────────────────── +add_action('wp_head', function() { + if (!fea_hide_static_meta()) return; + ?> + + post_name === 'escuela'; +} + +/** True para contenidos importados que funcionan como páginas institucionales. */ +function fea_hide_static_meta(): bool { + if (is_admin() || fea_is_front_page()) return false; + + $post = get_queried_object(); + if (!$post instanceof WP_Post) return false; + + if ($post->post_type === 'page') return true; + + $slugs = [ + 'colaboradores', + 'contactar', + 'multimedia', + 'ayuda', + 'video-tutorial', + 'como-usar-el-buscador-avanzado', + 'portal', + 'paraponeraldialafe', + 'alta', + 'alta-en-effa', + 'regala', + 'catalogo-de-publicaciones-2018', + 'nueva-politica-de-privacidad', + ]; + + return in_array($post->post_name, $slugs, true); +} + /** Devuelve el idioma actual de Polylang, o 'es' si no está activo. */ function fea_current_lang(): string { return (function_exists('pll_current_language') ? pll_current_language() : null) ?: 'es'; @@ -625,22 +710,26 @@ add_shortcode('fea_carta_semana_hero', function() { }); // ── Shortcode: [fea_articulos_semana] ───────────────────────────────────── +// Fuente principal: links de la sección "Artículos seleccionados" de la carta vigente. +// Fallbacks: ACF portada_articulos → últimos por categoría. add_shortcode('fea_articulos_semana', function($atts) { $lang = fea_current_lang(); $labels = fea_labels(); $atts = shortcode_atts(['titulo' => $labels['articulos']], $atts); $page = fea_front_page_id(); - // Selección editorial ACF (solo para ES; otros idiomas usan fallback) - $posts = []; - if ($lang === 'es' && function_exists('get_field')) { + // 1) Fuente principal: sección "Artículos seleccionados" de la carta vigente + $posts = function_exists('fea_carta_section_posts') ? fea_carta_section_posts('articulos', $lang) : []; + + // 2) Fallback ACF (solo ES) + if (empty($posts) && $lang === 'es' && function_exists('get_field')) { $seleccion = get_field('portada_articulos', $page) ?: []; foreach ($seleccion as $p) { if ($p->post_status === 'publish') $posts[] = $p; } } - // Fallback: últimos artículos en el idioma actual + // 3) Fallback: últimos artículos en el idioma actual if (empty($posts)) { $posts = get_posts([ 'posts_per_page' => 9, @@ -661,30 +750,33 @@ add_shortcode('fea_articulos_semana', function($atts) { }); // ── Shortcode: [fea_evangelio] ──────────────────────────────────────────── -// Editorial (cat 1646) primero, luego comentarios (cat 1647). Máx 7 en total. +// Fuente principal: sección "Evangelio y comentarios" de la carta vigente. +// Fallback: editorial cat 1646 + comentarios cat 1647 por fecha. add_shortcode('fea_evangelio', function($atts) { $lang = fea_current_lang(); $labels = fea_labels(); $atts = shortcode_atts(['titulo' => $labels['evangelio']], $atts); - $editorial = get_posts([ - 'posts_per_page' => 1, - 'category__in' => [fea_cat(1646)], - 'post_status' => 'publish', - 'orderby' => 'date', - 'order' => 'DESC', - ]); + $posts = function_exists('fea_carta_section_posts') ? fea_carta_section_posts('evangelio', $lang) : []; - $comentarios = get_posts([ - 'posts_per_page' => 6, - 'category__in' => [fea_cat(1647)], - 'category__not_in' => [fea_cat(1646)], - 'post_status' => 'publish', - 'orderby' => 'date', - 'order' => 'DESC', - ]); - - $posts = array_merge($editorial, $comentarios); + if (empty($posts)) { + $editorial = get_posts([ + 'posts_per_page' => 1, + 'category__in' => [fea_cat(1646)], + 'post_status' => 'publish', + 'orderby' => 'date', + 'order' => 'DESC', + ]); + $comentarios = get_posts([ + 'posts_per_page' => 6, + 'category__in' => [fea_cat(1647)], + 'category__not_in' => [fea_cat(1646)], + 'post_status' => 'publish', + 'orderby' => 'date', + 'order' => 'DESC', + ]); + $posts = array_merge($editorial, $comentarios); + } if (!$posts) return ''; $html = '
' @@ -695,18 +787,24 @@ add_shortcode('fea_evangelio', function($atts) { }); // ── Shortcode: [fea_eucaristia] ─────────────────────────────────────────── +// Fuente principal: sección "Para unas eucaristías más participativas" de la carta. +// Fallback: cat 1648 por fecha. add_shortcode('fea_eucaristia', function($atts) { $lang = fea_current_lang(); $labels = fea_labels(); $atts = shortcode_atts(['titulo' => $labels['eucaristia']], $atts); - $posts = get_posts([ - 'posts_per_page' => 6, - 'category__in' => [fea_cat(1648)], - 'post_status' => 'publish', - 'orderby' => 'date', - 'order' => 'DESC', - ]); + $posts = function_exists('fea_carta_section_posts') ? fea_carta_section_posts('eucaristia', $lang) : []; + + if (empty($posts)) { + $posts = get_posts([ + 'posts_per_page' => 6, + 'category__in' => [fea_cat(1648)], + 'post_status' => 'publish', + 'orderby' => 'date', + 'order' => 'DESC', + ]); + } if (!$posts) return ''; $html = '
' @@ -717,14 +815,17 @@ add_shortcode('fea_eucaristia', function($atts) { }); // ── Shortcode: [fea_multimedia] ─────────────────────────────────────────── +// Fuente principal: sección "Material multimedia" de la carta vigente. +// Fallbacks: ACF portada_multimedia → últimos por categoría. add_shortcode('fea_multimedia', function($atts) { $lang = fea_current_lang(); $labels = fea_labels(); $atts = shortcode_atts(['titulo' => $labels['multimedia']], $atts); $page = fea_front_page_id(); - $posts = []; - if ($lang === 'es' && function_exists('get_field')) { + $posts = function_exists('fea_carta_section_posts') ? fea_carta_section_posts('multimedia', $lang) : []; + + if (empty($posts) && $lang === 'es' && function_exists('get_field')) { $seleccion = get_field('portada_multimedia', $page) ?: []; foreach ($seleccion as $p) { if ($p->post_status === 'publish') $posts[] = $p; diff --git a/wordpress/wp-content/mu-plugins/fea-slider-sync.php b/wordpress/wp-content/mu-plugins/fea-slider-sync.php new file mode 100644 index 0000000..837f364 --- /dev/null +++ b/wordpress/wp-content/mu-plugins/fea-slider-sync.php @@ -0,0 +1,185 @@ + 0) return false; + } + + $files = fea_slider_home_files(); + + $uploads = wp_upload_dir(); + $reldir = '$upload$/' . FEA_SLIDER_DIRNAME; // SS3 variable placeholder + + // Leer slides actuales del slider 2 → mapeo imagen → row + $existing = $wpdb->get_results($wpdb->prepare( + "SELECT id, title, params, ordering FROM {$wpdb->prefix}nextend2_smartslider3_slides WHERE slider=%d ORDER BY ordering", + FEA_SLIDER_ID + ), ARRAY_A); + + $by_image = []; + foreach ($existing as $row) { + $p = json_decode($row['params'], true) ?: []; + $img = isset($p['backgroundImage']) ? basename($p['backgroundImage']) : null; + if ($img) $by_image[$img] = $row; + } + + $keep_ids = []; + $ordering = 0; + foreach ($files as $file) { + $ordering++; + if (isset($by_image[$file])) { + // Reutilizar slide existente — actualizar params si fuera necesario + $row = $by_image[$file]; + $p = json_decode($row['params'], true) ?: []; + $expected = $reldir . '/' . $file; + $needs_update = false; + if (($p['backgroundImage'] ?? '') !== $expected) { $p['backgroundImage'] = $expected; $needs_update = true; } + if (($p['background-type'] ?? '') !== 'image') { $p['background-type'] = 'image'; $needs_update = true; } + if ($needs_update) { + $wpdb->update( + $wpdb->prefix . 'nextend2_smartslider3_slides', + ['params' => wp_json_encode($p), 'ordering' => $ordering], + ['id' => $row['id']], + ['%s','%d'], ['%d'] + ); + } else { + $wpdb->update( + $wpdb->prefix . 'nextend2_smartslider3_slides', + ['ordering' => $ordering], + ['id' => $row['id']], + ['%d'], ['%d'] + ); + } + $keep_ids[] = (int) $row['id']; + } else { + // Crear slide nuevo + $title = pathinfo($file, PATHINFO_FILENAME); + $params = wp_json_encode([ + 'background-type' => 'image', + 'backgroundImage' => $reldir . '/' . $file, + 'version' => '3.5.1.32', + ]); + $img_url = $reldir . '/' . $file; + $wpdb->insert( + $wpdb->prefix . 'nextend2_smartslider3_slides', + [ + 'slider' => FEA_SLIDER_ID, + 'title' => $title, + 'description' => '', + 'params' => $params, + 'slide' => '[]', + 'thumbnail' => $img_url, + 'publish_up' => '1970-01-01 00:00:00', + 'publish_down' => '1970-01-01 00:00:00', + 'published' => 1, + 'first' => 0, + 'generator_id' => 0, + 'ordering' => $ordering, + ], + ['%d','%s','%s','%s','%s','%s','%s','%s','%d','%d','%d','%d'] + ); + $keep_ids[] = (int) $wpdb->insert_id; + } + } + + // Borrar slides cuya imagen ya no está en el directorio + if ($keep_ids) { + $in = implode(',', array_map('intval', $keep_ids)); + $wpdb->query($wpdb->prepare( + "DELETE FROM {$wpdb->prefix}nextend2_smartslider3_slides WHERE slider=%d AND id NOT IN ($in)", + FEA_SLIDER_ID + )); + } else { + $wpdb->query($wpdb->prepare( + "DELETE FROM {$wpdb->prefix}nextend2_smartslider3_slides WHERE slider=%d", + FEA_SLIDER_ID + )); + } + + update_option(FEA_SLIDER_OPT_KEY, $mtime); + + // Marcar slider como cambiado para que SS3 regenere su manifest + $wpdb->update( + $wpdb->prefix . 'nextend2_section_storage', + ['value' => (string) $mtime], + ['application' => 'smartslider', 'section' => 'sliderChanged', 'referenceKey' => (string) FEA_SLIDER_ID], + ['%s'], ['%s','%s','%s'] + ); + + return count($files); +} + +// Ejecutar sync en cada carga de portada (la comprobación de mtime evita trabajo si nada cambió). +add_action('template_redirect', function() { + if (!is_front_page()) return; + fea_slider_home_sync_now(); +}, 5); + +// Sync también al entrar al admin (por si el editor sube ficheros desde wp-admin). +add_action('admin_init', function() { + fea_slider_home_sync_now(); +}); + +// WP-CLI helper: `wp eval "fea_slider_home_sync_now(true);"` fuerza resync ignorando mtime.