From 9ac857b0274b208e61b8ff4f60dbb6fdfb37a5b3 Mon Sep 17 00:00:00 2001 From: rafa Date: Mon, 22 Jun 2026 08:30:18 -0400 Subject: [PATCH] =?UTF-8?q?fix(portada):=20art=C3=ADculos=20por=20idioma?= =?UTF-8?q?=20y=20enlace=20evangelio=20del=20d=C3=ADa=20(#132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 2: fea_carta_section_posts parsea SIEMPRE la carta ES (el parser solo reconoce cabeceras en español) y mapea cada artículo a su traducción Polylang del idioma de la portada. Antes EN/FR/IT/PT caían al fallback 'últimos por categoría' y mostraban artículos incorrectos. Bug 1: el enlace 'evangelio del día' resuelve la traducción del idioma actual con pll_get_post (get_posts usa suppress_filters=true y devolvía siempre la página ES). Desplegado y verificado en prod (wp-nuevo) 2026-06-22. Co-Authored-By: Claude Opus 4.8 --- .../mu-plugins/fea-carta-portada.php | 64 ++- .../wp-content/mu-plugins/fea-homepage.php | 543 +++++++++++++++++- 2 files changed, 562 insertions(+), 45 deletions(-) diff --git a/wordpress/wp-content/mu-plugins/fea-carta-portada.php b/wordpress/wp-content/mu-plugins/fea-carta-portada.php index c5461fc..74ac01f 100644 --- a/wordpress/wp-content/mu-plugins/fea-carta-portada.php +++ b/wordpress/wp-content/mu-plugins/fea-carta-portada.php @@ -124,20 +124,7 @@ function fea_resolve_links_in_html($html) { 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; - } - + // Joomla legacy: /item/-...html if (preg_match('~/item/(\d+)-[^/"]+\.html~i', $url, $m)) { $k2 = (int) $m[1]; if ($k2 > 0) { @@ -147,9 +134,36 @@ function fea_url_to_post_id($url) { )); if ($pid) return (int) $pid; } + return null; } - return null; + // 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) { + return null; // host externo → no es un artículo nuestro + } + + $path = (string) wp_parse_url($url, PHP_URL_PATH); + if ($path === '') return null; + $home_path = rtrim((string) wp_parse_url(home_url('/'), PHP_URL_PATH), '/'); + if ($home_path !== '' && strpos($path, $home_path . '/') === 0) { + $path = substr($path, strlen($home_path)); + } + $seg = explode('/', trim($path, '/')); + $slug = $seg[0] ?? ''; + if ($slug === '' || 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 + )); + return $pid ? (int) $pid : null; } /** @@ -157,13 +171,31 @@ function fea_url_to_post_id($url) { * 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) { + $lang = $lang ?: (function_exists('fea_current_lang') ? fea_current_lang() : 'es'); + + // El parser de secciones reconoce las cabeceras SOLO en español + // (fea_extract_sections_from_html). Las cartas traducidas tienen las + // cabeceras en su idioma → 0 secciones. Por eso parseamos SIEMPRE la + // carta ES y luego mapeamos cada link a su traducción del idioma destino. $carta_id = fea_get_current_carta_id($lang); if (!$carta_id) return []; - $sections = fea_parse_carta_sections($carta_id); + $carta_es = $carta_id; + if ($lang !== 'es' && function_exists('pll_get_post')) { + $es = pll_get_post($carta_id, 'es'); + if ($es) $carta_es = (int) $es; + } + + $sections = fea_parse_carta_sections($carta_es); $ids = $sections[$section_slug] ?? []; if (!$ids) return []; $posts = []; foreach ($ids as $pid) { + // Mapear el artículo ES a su traducción del idioma de la portada. + // Si no hay traducción, se mantiene el ES (degradación elegante). + if ($lang !== 'es' && function_exists('pll_get_post')) { + $tr = pll_get_post((int) $pid, $lang); + if ($tr) $pid = (int) $tr; + } $p = get_post($pid); if ($p && $p->post_status === 'publish') $posts[] = $p; } diff --git a/wordpress/wp-content/mu-plugins/fea-homepage.php b/wordpress/wp-content/mu-plugins/fea-homepage.php index ede84a1..fc08ee8 100755 --- a/wordpress/wp-content/mu-plugins/fea-homepage.php +++ b/wordpress/wp-content/mu-plugins/fea-homepage.php @@ -38,6 +38,74 @@ add_action('pre_get_posts', function(WP_Query $query) { $query->is_page = true; }, 1); +// ── Multimedia: 30 entradas por página en su archivo (#63) ── +add_action('pre_get_posts', function(WP_Query $query) { + if (is_admin() || !$query->is_main_query()) return; + if ($query->is_category('multimedia')) { + $query->set('posts_per_page', 30); + } +}); + +// ── Tablón de anuncios (#97): ocultar del listado los anuncios de más de 12 meses ── +// Ventana móvil, no destructiva: los posts siguen publicados/accesibles por URL; solo +// se excluyen del archivo de la categoría para que el Tablón no parezca desactualizado. +// Cubre las 5 categorías Polylang (ES + en/fr/it/pt). +add_action('pre_get_posts', function(WP_Query $query) { + if (is_admin() || !$query->is_main_query()) return; + if ($query->is_category(['tablon-de-anuncios', 'tablon-de-anuncios-en', + 'tablon-de-anuncios-fr', 'tablon-de-anuncios-it', 'tablon-de-anuncios-pt'])) { + $query->set('date_query', [[ + 'after' => date('Y-m-d', strtotime('-12 months')), + 'inclusive' => true, + ]]); + } +}); + +// ── Normalizar títulos (TODO CAPS legacy → frase) en todo el front #63 #73 ── +// Mismo criterio que la portada (fea_title). Cubre listados/búsqueda/home, el artículo +// (single), el /SEO y los feeds. El wp-admin se deja SIN tocar para que el editor +// vea el dato real (en mayúsculas) al editar. +add_filter('the_title', function($title, $post_id = 0) { + if (is_admin() || !function_exists('fea_title')) return $title; + // Artículo individual: solo el título del propio post mostrado (no widgets/relacionados). + if (is_singular()) { + if (in_the_loop() || (int) $post_id === (int) get_queried_object_id()) { + return fea_title($title); + } + return $title; + } + // Listados (incluida portada) dentro del loop. + if ((is_archive() || is_search() || is_home() || is_front_page()) && in_the_loop()) { + return fea_title($title); + } + return $title; +}, 20, 2); + +// <title> del documento — núcleo WP (cuando Yoast no lo sobrescribe). +add_filter('document_title_parts', function($parts) { + if (is_admin() || !function_exists('fea_title')) return $parts; + if (!empty($parts['title'])) $parts['title'] = fea_title($parts['title']); + return $parts; +}, 20); + +// <title>/OG/Twitter vía Yoast: normaliza solo la porción del título del post. +$fea_seo_title = function($title) { + if (is_admin() || !is_singular() || !function_exists('fea_title')) return $title; + $raw = get_post_field('post_title', get_queried_object_id()); + if ($raw && mb_strpos($title, $raw) !== false) { + $title = str_replace($raw, fea_title($raw), $title); + } + return $title; +}; +add_filter('wpseo_title', $fea_seo_title, 20); +add_filter('wpseo_opengraph_title', $fea_seo_title, 20); +add_filter('wpseo_twitter_title', $fea_seo_title, 20); + +// Título del item en feeds RSS. +add_filter('the_title_rss', function($title) { + return function_exists('fea_title') ? fea_title($title) : $title; +}, 20); + // Asegura que option_page_on_front devuelve la página traducida al idioma actual // (necesario para is_front_page() y para que el template FSE lo reconozca) add_filter('option_page_on_front', function($value) { @@ -267,6 +335,8 @@ add_action('wp_head', function() { } #fea-lang-dropdown a:hover { background: #f5f5f5; } #fea-lang-dropdown a[aria-current] { font-weight: 700; color: #000; background: #efefef; } + #fea-lang-dropdown a.fea-lang-untrans { opacity: 0.45; } + #fea-lang-dropdown a.fea-lang-untrans::after { content: "·"; margin-left: 4px; opacity: 0.7; } </style> <?php }); @@ -290,7 +360,12 @@ add_action('wp_footer', function() { $flag = $flags[$l['slug']] ?? '🌐'; $code = strtoupper($l['slug']); $cur = $l['current_lang'] ? ' aria-current="true"' : ''; - $items .= '<li><a href="' . esc_url($l['url']) . '"' . $cur . '>' . $flag . ' ' . $code . '</a></li>'; + // Marcar los idiomas SIN traducción de esta página (Polylang enlaza al inicio del + // idioma): se atenúan y avisan, para no hacer creer que traducen el contenido actual. + $notr = !empty($l['no_translation']) && empty($l['current_lang']); + $cls = $notr ? ' class="fea-lang-untrans"' : ''; + $ttl = $notr ? ' title="Esta página no está traducida — irás al inicio en ' . esc_attr($code) . '"' : ''; + $items .= '<li><a href="' . esc_url($l['url']) . '"' . $cur . $cls . $ttl . '>' . $flag . ' ' . $code . '</a></li>'; } ?> <div id="fea-lang-switcher" role="navigation" aria-label="Idioma / Language"> @@ -402,30 +477,151 @@ add_action('wp_head', function() { add_action('wp_head', function() { if (!fea_is_front_page()) return; ?> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> + <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&display=swap" rel="stylesheet"> <style> - .fea-hero { border-bottom: 2px solid #111; padding-bottom: 2rem; margin-bottom: 2.5rem; } + /* ── Hero: Carta (izq) + Carrusel (der) en banda beige full-width ── */ + .fea-hero-band { max-width: none !important; width: 100vw; margin-left: calc(50% - 50vw) !important; margin-right: calc(50% - 50vw) !important; background: linear-gradient(180deg, #efe9e1, #f4f0ea); border-bottom: 1px solid #e4ddd1; margin-bottom: 3rem; } + .fea-hero-inner { max-width: 1180px; margin: 0 auto; padding: 2.75rem 28px; display: grid; grid-template-columns: minmax(260px, 400px) minmax(0, 1fr); gap: 2.5rem; align-items: center; } + .fea-hero-text { min-width: 0; } .fea-hero-link { display: block; text-decoration: none; color: inherit; } .fea-hero-link:hover .fea-hero-title { text-decoration: underline; text-underline-offset: 4px; } - .fea-section-label { display: inline-block; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: #888; margin-bottom: 0.6rem; } - .fea-hero-title { font-size: clamp(1.5rem, 4vw, 2.2rem); font-weight: 700; line-height: 1.2; margin: 0 0 0.75rem; color: #111; } - .fea-hero-meta { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; color: #666; } + .fea-section-label { display: inline-block; font-size: 0.72rem; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase; color: #8b1a2e; margin-bottom: 0.7rem; } + .fea-hero-title { font-family: 'Fraunces', Georgia, serif; font-size: clamp(2rem, 3.4vw, 3rem); font-weight: 600; line-height: 1.08; letter-spacing: -0.01em; margin: 0 0 1rem; color: #2a2320; } + .fea-hero-meta { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; color: #6f655c; } + .fea-hero-cta { display: inline-block; margin-top: 1.25rem; background: #8b1a2e; color: #fff; font-weight: 600; font-size: 0.92rem; padding: 0.7rem 1.4rem; border-radius: 8px; } + .fea-hero-slider { min-width: 0; overflow: hidden; } + .fea-hero-slider .n2-ss-align { max-width: 100% !important; width: 100% !important; } + @media (max-width: 860px) { .fea-hero-inner { grid-template-columns: 1fr; gap: 1.75rem; } } - .fea-section { margin-bottom: 4.5rem; padding-top: 0.5rem; } - .fea-section-title { font-size: 0.85rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: #333; margin: 0 0 1.5rem; padding: 0.4rem 0 0.55rem 0.8rem; border-left: 3px solid #8b1a2e; border-bottom: 1px solid #e0e0e0; } + /* Ancho de las secciones de portada: contenedor centrado (aire a los lados) */ + body.home .fea-section, .fea-front .fea-section { max-width: 1180px !important; margin-left: auto !important; margin-right: auto !important; } - .fea-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; } - @media (max-width: 720px) { .fea-grid { grid-template-columns: 1fr 1fr; } } - @media (max-width: 480px) { .fea-grid { grid-template-columns: 1fr; } } + /* ── Secciones — dirección "Cálido editorial" (Mockup A, #57) ── */ + .fea-section { margin-bottom: 3.75rem; padding-top: 0.5rem; } + .fea-section-title { display: flex; align-items: center; gap: 1rem; font-family: 'Fraunces', Georgia, serif; font-size: 1.65rem; font-weight: 600; letter-spacing: -0.01em; text-transform: none; color: #2a2320; margin: 0 0 1.6rem; padding: 0; border: 0; } + .fea-section-title::after { content: ''; display: inline-block; flex: 0 0 46px; height: 3px; background: #8b1a2e; border-radius: 2px; } + .fea-section-head { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.6rem; } + .fea-section-head .fea-section-title { flex: 1 1 auto; min-width: 0; margin-bottom: 0; } + .fea-section-more { flex: 0 0 auto; color: #8b1a2e; font-size: 0.86rem; font-weight: 700; text-decoration: none; white-space: nowrap; } + .fea-section-more:hover { text-decoration: underline; text-underline-offset: 3px; } + @media (max-width: 480px) { + .fea-section-head { gap: 0.65rem; } + .fea-section-head .fea-section-title { font-size: 1.45rem; gap: 0.65rem; } + .fea-section-head .fea-section-title::after { flex-basis: 28px; } + .fea-section-more { font-size: 0.8rem; } + } - .fea-card { border-bottom: 1px solid #e5e5e5; padding-bottom: 1.1rem; } + .fea-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.6rem; } + @media (max-width: 980px) { .fea-grid { grid-template-columns: repeat(3, 1fr); } } + @media (max-width: 680px) { .fea-grid { grid-template-columns: 1fr 1fr; } } + @media (max-width: 420px) { .fea-grid { grid-template-columns: 1fr; } } + + .fea-card { background: #fff; border: 1px solid #efe7d8; border-radius: 14px; padding: 1.5rem 1rem; text-align: center; transition: transform .18s ease, box-shadow .18s ease; } + .fea-card:hover { transform: translateY(-5px); box-shadow: 0 18px 36px -20px rgba(42,35,32,.45); } /* Ocultar visualmente el texto de accesibilidad del Smart Slider (nombres de fichero con guiones bajos) */ .n2-ss-slide--focus { position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; margin: -1px !important; overflow: hidden !important; clip: rect(0,0,0,0) !important; white-space: nowrap !important; border: 0 !important; } - .fea-card-meta { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.4rem; } - .fea-avatar { border-radius: 50%; width: 28px !important; height: 28px !important; flex-shrink: 0; display: inline-block !important; } - .fea-card-author { font-size: 0.78rem; font-weight: 600; color: #555; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .fea-card-title { font-size: 0.93rem; font-weight: 600; line-height: 1.35; margin: 0; } - .fea-card-title a { text-decoration: none; color: #111; } + .fea-card-avatar-link { display: block; } + .fea-card-avatar { box-shadow: 0 0 0 3px #f7ecd9, 0 0 0 4px #e7d7bb; transition: transform .2s ease; } + .fea-card:hover .fea-card-avatar { transform: scale(1.05); } + .fea-card-author { color: #8b1a2e; font-weight: 600; font-size: 0.9rem; margin: 0.95rem 0 0.5rem; line-height: 1.25; } + .fea-card-title { font-size: 0.98rem; font-weight: 600; line-height: 1.32; margin: 0; } + .fea-card-title a { text-decoration: none; color: #2a2320; } .fea-card-title a:hover { text-decoration: underline; text-underline-offset: 3px; } + + /* ── Footer portada: contener anchos, aire a los lados (no entre medias) ── */ + /* Reduce el espaciado preset 80 (enorme) solo dentro del footer, sin tocar listas */ + .fea-footer-portada { max-width: 1180px; margin-left: auto; margin-right: auto; --wp--preset--spacing--80: 3rem; } + /* Las 3 imágenes (Librería / Noticias / Portal-EFFA): columnas iguales, aires iguales */ + .fea-footer-portada .wp-block-columns { max-width: 900px; margin-left: auto; margin-right: auto; gap: 2.5rem !important; align-items: start; justify-content: center; } + .fea-footer-portada .wp-block-columns .wp-block-column { flex: 1 1 0 !important; width: auto !important; min-width: 0; display: flex; flex-direction: column; align-items: center; text-align: center; } + .fea-footer-portada .wp-block-columns .wp-block-column img { max-width: 100%; width: auto; height: auto; margin-left: auto; margin-right: auto; } + /* Las columnas de enlaces: centrarlas (aire a los lados, no entre medias) */ + .fea-footer-portada .wp-block-group.is-content-justification-space-between { justify-content: center !important; gap: 3.5rem !important; } + </style> + <?php +}); + +// ── Slider del hero a prueba de fuego: llena la columna y, si es más estrecha +// que el ancho base del slider (Smart Slider no baja de él), lo escala con transform ── +add_action('wp_footer', function() { + if (!fea_is_front_page()) return; + ?> + <script> + (function () { + var els = function () { + return { + c: document.querySelector('.fea-hero-slider'), + s: document.querySelector('.fea-hero-slider .n2-ss-slider') + }; + }; + var lastW = -1, busy = false, raf = 0; + function apply() { + var e = els(); + if (!e.c || !e.s) return; + // reset para medir el tamaño natural que decide Smart Slider + e.s.style.transform = ''; + e.s.style.transformOrigin = ''; + e.c.style.height = ''; + // que Smart Slider recalcule (crece hasta llenar la columna si cabe) + busy = true; window.dispatchEvent(new Event('resize')); busy = false; + requestAnimationFrame(function () { + var avail = e.c.clientWidth; + var rect = e.s.getBoundingClientRect(); + if (rect.width > avail + 1) { // columna más estrecha que el slider → escalar + var k = avail / rect.width; + e.s.style.transformOrigin = 'top left'; + e.s.style.transform = 'scale(' + k + ')'; + e.c.style.height = Math.round(rect.height * k) + 'px'; + } + }); + } + function schedule() { cancelAnimationFrame(raf); raf = requestAnimationFrame(apply); } + function onChange(force) { + if (busy) return; + var c = els().c; if (!c) return; + var w = c.clientWidth; + if (!force && w === lastW) return; // solo reaccionar a cambios de ANCHO (evita bucle por la altura) + lastW = w; schedule(); + } + window.addEventListener('load', function () { onChange(true); setTimeout(function(){onChange(true);}, 300); setTimeout(function(){onChange(true);}, 800); }); + window.addEventListener('resize', function () { onChange(false); }); + if ('ResizeObserver' in window) { + var c = els().c; + if (c) new ResizeObserver(function () { onChange(false); }).observe(c); + } + })(); + </script> + <?php +}, 99); + +// ── Estilos para listados/archivos (categoría, autor, fecha, búsqueda) #63 ── +add_action('wp_head', function() { + if (is_admin() || fea_is_front_page()) return; + if (!(is_archive() || is_search() || is_home())) return; + ?> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&display=swap" rel="stylesheet"> + <style> + /* Título del archivo: serif con barra granate (coherente con la portada) */ + .wp-block-query-title { font-family: 'Fraunces', Georgia, serif !important; font-weight: 600 !important; color: #2a2320 !important; letter-spacing: -0.01em; display: flex; align-items: center; gap: 1rem; } + .wp-block-query-title::after { content: ''; flex: 0 0 46px; height: 3px; background: #8b1a2e; border-radius: 2px; } + /* Anchura coherente con la portada */ + .wp-block-query.alignwide { max-width: 1180px !important; margin-left: auto !important; margin-right: auto !important; } + /* Grid de tarjetas */ + .fea-archive-grid { display: grid !important; grid-template-columns: repeat(3, 1fr) !important; gap: 1.5rem !important; } + @media (max-width: 900px) { .fea-archive-grid { grid-template-columns: repeat(2, 1fr) !important; } } + @media (max-width: 560px) { .fea-archive-grid { grid-template-columns: 1fr !important; } } + .fea-archive-card { background: #fff; border: 1px solid #efe7d8; border-radius: 14px; padding: 1.4rem 1.3rem; height: 100%; display: flex; flex-direction: column; transition: transform .18s ease, box-shadow .18s ease; } + .fea-archive-card:hover { transform: translateY(-4px); box-shadow: 0 18px 36px -20px rgba(42,35,32,.45); } + .fea-archive-title { margin: 0 !important; } + .fea-archive-title a { font-family: 'Fraunces', Georgia, serif; font-weight: 600; color: #2a2320; text-decoration: none; line-height: 1.25; } + .fea-archive-card:hover .fea-archive-title a { color: #8b1a2e; } + .fea-archive-date { margin: 0.45rem 0 0 !important; } + .fea-archive-date a, .fea-archive-date { color: #8b1a2e !important; font-weight: 600; text-decoration: none; } + .fea-archive-excerpt { margin: 0.6rem 0 0 !important; color: #6f655c; font-size: 0.92rem; line-height: 1.45; } + .fea-archive-excerpt a { display: none; } </style> <?php }); @@ -473,6 +669,14 @@ add_filter('the_content', function($content) { return '<h1 class="fea-sr-only fea-page-h1">' . esc_html($title) . '</h1>' . $content; }, 5); +// ── Título <title> para páginas sin post_title propio (Escuela) ─────────── +add_filter('document_title_parts', function($parts) { + if (fea_is_escuela_page()) { + $parts['title'] = 'Escuela de Formación en Fe Adulta'; + } + return $parts; +}); + // ── Ocultar metadatos de post en páginas estáticas ──────────────────────── add_action('wp_head', function() { if (!fea_hide_static_meta()) return; @@ -510,7 +714,7 @@ add_action('astra_single_header_bottom', function() { echo '<div class="fea-byline">' . '<a href="' . esc_url($author_url) . '" class="fea-byline-avatar-link">' - . '<img src="' . esc_url($avatar_url) . '" alt="" width="48" height="48" class="fea-byline-avatar">' + . '<img src="' . esc_url($avatar_url) . '" alt="' . esc_attr($author_name) . '" width="48" height="48" class="fea-byline-avatar">' . '</a>' . '<div class="fea-byline-info">' . '<a href="' . esc_url($author_url) . '" class="fea-byline-name">' . esc_html($author_name) . '</a>' @@ -531,34 +735,135 @@ add_action('wp_head', function() { .fea-byline-name:hover { text-decoration: underline; } .fea-byline-cat { font-size: 0.78rem; color: #888; text-decoration: none; } .fea-byline-cat:hover { text-decoration: underline; color: #555; } + .wp-block-group:has(> .wp-block-avatar):has(.wp-block-post-author-name) { + align-items: center; + gap: 0.7rem; + } + .wp-block-group:has(> .wp-block-avatar):has(.wp-block-post-author-name) > .wp-block-avatar { + flex: 0 0 54px; + } + .wp-block-group:has(> .wp-block-avatar):has(.wp-block-post-author-name) > .wp-block-avatar img { + width: 54px !important; + height: 54px !important; + } + .fea-post-date-inline { + font-size: 0.74rem; + line-height: 1.2; + color: #888; + margin-top: 0.08rem; + } + .fea-post-date-inline time { white-space: nowrap; } + @media (max-width: 720px) { + .wp-block-group:has(> .wp-block-avatar):has(.wp-block-post-author-name) > .wp-block-avatar { + flex-basis: 50px; + } + .wp-block-group:has(> .wp-block-avatar):has(.wp-block-post-author-name) > .wp-block-avatar img { + width: 50px !important; + height: 50px !important; + } + } </style> <?php }); +add_filter('render_block_core/post-terms', function($block_content, $block) { + if (!is_single() || get_post_type() !== 'post') return $block_content; + + $taxonomy = $block['attrs']['term'] ?? ''; + if ($taxonomy !== 'category') return $block_content; + if (strpos($block_content, 'fea-post-date-inline') !== false) return $block_content; + + $date_attr = esc_attr(get_the_date('c')); + $date_text = esc_html(ucfirst(wp_date(get_option('date_format'), get_post_timestamp()))); + if ($date_text === '') return $block_content; + + return $block_content + . '<div class="fea-post-date-inline"><time datetime="' . $date_attr . '">' . $date_text . '</time></div>'; +}, 10, 2); + // ── Helpers ─────────────────────────────────────────────────────────────── function fea_title(string $title): string { $lower = mb_strtolower($title, 'UTF-8'); - return mb_strtoupper(mb_substr($lower, 0, 1, 'UTF-8'), 'UTF-8') . mb_substr($lower, 1, null, 'UTF-8'); + $out = mb_strtoupper(mb_substr($lower, 0, 1, 'UTF-8'), 'UTF-8') . mb_substr($lower, 1, null, 'UTF-8'); + // Capitalizar también la primera letra tras separadores de cláusula (citas bíblicas + // dobles "Isaías 5,1 / Filipenses 4,6", subtítulos "Título: Subtítulo", ¿…?, ¡…!). + $out = preg_replace_callback('/([\/:¿¡] *)(\p{Ll})/u', function ($m) { + return $m[1] . mb_strtoupper($m[2], 'UTF-8'); + }, $out); + return $out; +} + +/** Lista de libros bíblicos (para avatar genérico de lecturas/eucaristías). #61 */ +function fea_libros_biblicos(): array { + return [ + 'Nuevo Testamento','Antiguo Testamento', + 'Génesis','Éxodo','Levítico','Números','Deuteronomio','Josué','Jueces','Rut', + 'Samuel','Reyes','Crónicas','Esdras','Nehemías','Tobías','Judit','Ester','Macabeos', + 'Job','Salmos','Salmo','Proverbios','Eclesiastés','Cantar','Sabiduría','Eclesiástico','Sirácide', + 'Isaías','Jeremías','Lamentaciones','Baruc','Ezequiel','Daniel', + 'Oseas','Joel','Amós','Abdías','Jonás','Miqueas','Nahúm','Habacuc','Sofonías','Ageo','Zacarías','Malaquías', + 'Hechos','Romanos','Corintios','Gálatas','Efesios','Filipenses','Colosenses','Tesalonicenses', + 'Timoteo','Tito','Filemón','Hebreos','Santiago','Pedro','Judas','Apocalipsis', + ]; +} + +/** Devuelve la abreviatura del evangelista si el texto es una cita de evangelio, o ''. #61 */ +function fea_evangelista_de_texto(string $txt): string { + if (preg_match('/^\s*(Mateo|Mt|Marcos|Mc|Lucas|Lc|Juan|Jn)\b\.?\s*\d/iu', $txt, $m)) { + $k = mb_strtolower($m[1], 'UTF-8'); + $map = ['mateo'=>'mateo-angel','mt'=>'mateo-angel','marcos'=>'marcos-leon','mc'=>'marcos-leon', + 'lucas'=>'lucas-toro','lc'=>'lucas-toro','juan'=>'juan-aguila','jn'=>'juan-aguila']; + return $map[$k] ?? ''; + } + return ''; +} + +/** True si el texto empieza por un libro bíblico (cita) o es el nombre de un libro. #61 */ +function fea_es_libro_biblico(string $txt): bool { + $txt = trim($txt); + foreach (fea_libros_biblicos() as $libro) { + if (mb_strtolower($txt, 'UTF-8') === mb_strtolower($libro, 'UTF-8')) return true; + if (preg_match('/^' . preg_quote($libro, '/') . '\b/iu', $txt)) return true; + } + return false; +} + +/** + * URL del avatar para un post de portada/listado. #61 + * - Citas de evangelio (Mt/Mc/Lc/Jn + número) → símbolo del evangelista. + * - Lecturas/eucaristías firmadas por un libro bíblico → Biblia. + * - Resto → avatar real del autor. + */ +function fea_avatar_url(object $post, int $size, int $author_id, string $author_name): string { + $base = content_url('uploads/avatares/evangelistas/'); + // Solo la LECTURA en sí (cuyo título es la cita, p.ej. "Mateo 9, 36") lleva símbolo de + // evangelista; los comentaristas humanos conservan su avatar aunque comenten ese evangelio. + $title = (string) $post->post_title; + if ($ev = fea_evangelista_de_texto($title)) return $base . $ev . '.svg'; + if (fea_es_libro_biblico($title) || fea_es_libro_biblico($author_name)) return $base . 'biblia.svg'; + return get_avatar_url($author_id, ['size' => $size, 'default' => 'identicon']); } function fea_card(object $post): string { $author_id = $post->post_author; $author_name = get_the_author_meta('display_name', $author_id); - $avatar_url = get_avatar_url($author_id, ['size' => 40, 'default' => 'identicon']); + $avatar_url = fea_avatar_url($post, 96, (int) $author_id, (string) $author_name); $url = get_permalink($post->ID); $title = fea_title($post->post_title); return '<article class="fea-card">' - . '<div class="fea-card-meta">' - . '<img src="' . esc_url($avatar_url) . '" alt="" width="28" height="28" class="fea-avatar" loading="lazy">' - . '<span class="fea-card-author">' . esc_html($author_name) . '</span>' - . '</div>' + . '<a href="' . esc_url($url) . '" class="fea-card-avatar-link" aria-label="' . esc_attr($author_name) . '">' + . '<span class="fea-card-avatar" style="display:block;width:84px;height:84px;border-radius:50%;overflow:hidden;margin:0 auto;">' + . '<img src="' . esc_url($avatar_url) . '" alt="' . esc_attr($author_name) . '" width="84" height="84" class="fea-avatar" style="width:100%;height:100%;object-fit:cover;display:block;" loading="lazy">' + . '</span>' + . '</a>' + . '<div class="fea-card-author">' . esc_html($author_name) . '</div>' . '<h3 class="fea-card-title"><a href="' . esc_url($url) . '">' . esc_html($title) . '</a></h3>' . '</article>'; } /** IDs de las páginas de portada en todos los idiomas. */ function fea_front_page_ids(): array { - return [26542, 42756, 42757, 42758, 43889]; // ES, FR, IT, PT, EN + return [26542, 42756, 42757, 42758, 43889]; // ES=26542, PT=42756, IT=42757, FR=42758, EN=43889 (FR/PT corregidos #75) } /** True si la página actual es alguna de las portadas (multilingüe). */ @@ -678,6 +983,57 @@ function fea_cat(int $es_cat_id): int { return $es_cat_id; } +// ── Enlace "Evangelio del día · fecha" ──────────────────────────────────── +// Píldora discreta, estilo "fecha con contenido", a la página del Evangelio de +// cada día. Se inserta dentro del hero de la carta (portada). Multiidioma. +function fea_eed_link_html() { + $tz = new DateTimeZone('Europe/Madrid'); + $now = new DateTimeImmutable('now', $tz); + $d = (int) $now->format('j'); + $m = (int) $now->format('n'); + $lang = function_exists('fea_current_lang') ? fea_current_lang() : 'es'; + + $LBL = ['es'=>'Evangelio del día','en'=>'Gospel of the day','fr'=>'Évangile du jour','it'=>'Vangelo del giorno','pt'=>'Evangelho do dia']; + $MES = [ + 'es'=>[1=>'enero','febrero','marzo','abril','mayo','junio','julio','agosto','septiembre','octubre','noviembre','diciembre'], + 'en'=>[1=>'January','February','March','April','May','June','July','August','September','October','November','December'], + 'fr'=>[1=>'janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'], + 'it'=>[1=>'gennaio','febbraio','marzo','aprile','maggio','giugno','luglio','agosto','settembre','ottobre','novembre','dicembre'], + 'pt'=>[1=>'janeiro','fevereiro','março','abril','maio','junho','julho','agosto','setembro','outubro','novembro','dezembro'], + ]; + $L = isset($MES[$lang]) ? $lang : 'es'; + $mes = $MES[$L][$m]; + if ($L === 'es' || $L === 'pt') $fecha = $d . ' de ' . $mes; + elseif ($L === 'en') $fecha = $mes . ' ' . $d; + else $fecha = $d . ' ' . $mes; // fr, it + + // get_posts usa suppress_filters=true → Polylang no filtra y siempre + // devuelve la página ES. Resolvemos la traducción del idioma actual. + $pg = get_posts(['name'=>'evangelio-de-cada-dia','post_type'=>'post','post_status'=>'publish','numberposts'=>1]); + $pid = $pg ? (int) $pg[0]->ID : 0; + if ($pid && $lang !== 'es' && function_exists('pll_get_post')) { + $tr = pll_get_post($pid, $lang); + if ($tr) $pid = (int) $tr; + } + $url = $pid ? get_permalink($pid) : home_url('/evangelio-de-cada-dia/'); + + $css = '<style>' + . '.fea-eed-link{margin:1.6rem 0 0}' + . '.fea-eed-link a{display:inline-flex;align-items:center;gap:.5rem;color:#8b1a2e;text-decoration:none;font-size:.92rem;line-height:1}' + . '.fea-eed-link a:hover{text-decoration:underline;text-underline-offset:3px}' + . '.fea-eed-link .lbl{font-weight:700}' + . '.fea-eed-link .sep{color:#caa9b0}' + . '.fea-eed-link .fecha{color:#6f655c}' + . '.fea-eed-link .arrow{color:#8b1a2e;font-weight:700}' + . '</style>'; + + return $css . '<div class="fea-eed-link"><a href="' . esc_url($url) . '">' + . '<span class="lbl">' . esc_html($LBL[$L]) . '</span>' + . '<span class="sep">·</span>' + . '<span class="fecha">' . esc_html($fecha) . '</span>' + . '<span class="arrow">›</span></a></div>'; +} + // ── Shortcode: [fea_carta_semana_hero] ──────────────────────────────────── add_shortcode('fea_carta_semana_hero', function() { $lang = fea_current_lang(); @@ -697,16 +1053,23 @@ add_shortcode('fea_carta_semana_hero', function() { $fecha = date_i18n('j F Y', strtotime($c->post_date)); $author_name = get_the_author_meta('display_name', $c->post_author); $avatar_url = get_avatar_url($c->post_author, ['size' => 32, 'default' => 'identicon']); + $slider = do_shortcode('[smartslider3 slider="2"]'); - return '<section class="fea-hero">' + return '<section class="fea-hero-band"><div class="fea-hero-inner">' + . '<div class="fea-hero-text">' . '<a href="' . esc_url($url) . '" class="fea-hero-link">' . '<span class="fea-section-label">' . esc_html($labels['carta']) . '</span>' . '<h2 class="fea-hero-title">' . esc_html(fea_title($c->post_title)) . '</h2>' . '<div class="fea-hero-meta">' - . '<img src="' . esc_url($avatar_url) . '" alt="" width="32" height="32" class="fea-avatar">' + . '<img src="' . esc_url($avatar_url) . '" alt="' . esc_attr($author_name) . '" width="32" height="32" class="fea-avatar" style="width:32px;height:32px;border-radius:50%;object-fit:cover;">' . '<span>' . esc_html($author_name) . ' · ' . $fecha . '</span>' . '</div>' - . '</a></section>'; + . '<span class="fea-hero-cta">' . esc_html($labels['carta'] ? 'Leer la carta' : 'Leer la carta') . ' →</span>' + . '</a>' + . fea_eed_link_html() + . '</div>' + . '<div class="fea-hero-slider">' . $slider . '</div>' + . '</div></section>'; }); // ── Shortcode: [fea_articulos_semana] ───────────────────────────────────── @@ -786,6 +1149,100 @@ add_shortcode('fea_evangelio', function($atts) { return $html . '</div></section>'; }); +// ── Shortcode: [fea_evangelio_diario] ───────────────────────────────────── +// "El Evangelio de cada día": dos devocionales diarios indexados por día del año. +// · A la fuente cada día (texto, Fray Marcos) → categoría term_id 14 +// · Otro evangelio es posible (vídeo YouTube) → categoría term_id 15 +// Los posts están titulados "D mes" (ej. "21 junio"). Se muestra el de HOY +// (zona horaria del sitio) o el día indicado por ?fed=M-D, con pestañas para +// que el usuario elija formato y navegación día anterior / siguiente. +// Contenido solo en ES (devocional sin traducción) → categorías 14/15 fijas. +add_shortcode('fea_evangelio_diario', function($atts) { + $MESES = [1=>'enero',2=>'febrero',3=>'marzo',4=>'abril',5=>'mayo',6=>'junio', + 7=>'julio',8=>'agosto',9=>'septiembre',10=>'octubre',11=>'noviembre',12=>'diciembre']; + // Día litúrgico de referencia: España (la web es ES); el servidor va en UTC. + $tz = new DateTimeZone('Europe/Madrid'); + $now = new DateTimeImmutable('now', $tz); + $m = (int) $now->format('n'); + $d = (int) $now->format('j'); + if (!empty($_GET['fed']) && preg_match('/^(\d{1,2})-(\d{1,2})$/', $_GET['fed'], $mm)) { + $gm = (int) $mm[1]; $gd = (int) $mm[2]; + if ($gm >= 1 && $gm <= 12 && $gd >= 1 && $gd <= 31) { $m = $gm; $d = $gd; } + } + $titulo_dia = $d . ' ' . $MESES[$m]; // "21 junio" + + $find = function($cat) use ($titulo_dia) { + global $wpdb; + $id = $wpdb->get_var($wpdb->prepare( + "SELECT p.ID FROM {$wpdb->posts} p + JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID + JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id + WHERE tt.taxonomy = 'category' AND tt.term_id = %d + AND p.post_type = 'post' AND p.post_status = 'publish' + AND LOWER(TRIM(p.post_title)) = %s + ORDER BY p.ID ASC LIMIT 1", + $cat, $titulo_dia)); + return $id ? get_post((int) $id) : null; + }; + $texto = $find(14); + $video = $find(15); + + // navegación por día del calendario (año irrelevante; usamos un año bisiesto fijo) + $base = get_permalink(); + $cur = DateTimeImmutable::createFromFormat('!Y-n-j', '2024-' . $m . '-' . $d, $tz); + $prev = $cur->modify('-1 day'); + $next = $cur->modify('+1 day'); + $lbl = function($dt) use ($MESES) { return ((int)$dt->format('j')) . ' ' . substr($MESES[(int)$dt->format('n')], 0, 3); }; + $href = function($dt) use ($base) { return esc_url(add_query_arg('fed', $dt->format('n') . '-' . $dt->format('j'), $base)); }; + + $render = function($post, $tipo) { + if (!$post) { + return '<p class="fea-eed-empty">No hay ' . ($tipo === 'video' ? 'vídeo' : 'texto') . ' disponible para este día.</p>'; + } + $c = apply_filters('the_content', $post->post_content); + return '<div class="fea-eed-art">' . $c . '</div>'; + }; + + $css = '<style>' + . '.fea-eed{max-width:760px;margin:0 auto}' + . '.fea-eed-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;margin:.2rem 0 1.1rem}' + . '.fea-eed-date{font-size:1.15rem;color:#8b1a2e;margin:0;text-align:center;flex:1}' + . '.fea-eed-nav{white-space:nowrap;color:#8b1a2e;text-decoration:none;font-weight:600;font-size:.9rem;padding:.3rem .6rem;border:1px solid #e2cdd2;border-radius:8px}' + . '.fea-eed-nav:hover{background:#faf3f4}' + . '.fea-eed-tabs input{position:absolute;opacity:0;pointer-events:none}' + . '.fea-eed-labels{display:flex;gap:.5rem;border-bottom:2px solid #e2cdd2;margin-bottom:1.2rem}' + . '.fea-eed-labels label{flex:1;text-align:center;cursor:pointer;padding:.6rem 1rem;font-weight:700;color:#888;border:2px solid transparent;border-bottom:none;border-radius:8px 8px 0 0;margin-bottom:-2px}' + . '#fed-texto:checked~.fea-eed-labels label[for=fed-texto],' + . '#fed-video:checked~.fea-eed-labels label[for=fed-video]{color:#8b1a2e;border-color:#e2cdd2;background:#faf3f4}' + . '.fea-eed-panel{display:none}' + . '#fed-texto:checked~.fea-eed-panel-texto{display:block}' + . '#fed-video:checked~.fea-eed-panel-video{display:block}' + . '.fea-eed-art iframe{max-width:100%}' + . '.fea-eed-panel-video .fea-eed-art iframe{width:100%;max-width:600px;height:auto;aspect-ratio:16/9;display:block;margin:0 auto}' + . '.fea-eed-art h1{font-size:1.4rem;line-height:1.3;text-align:center}' + . '.fea-eed-empty{color:#888;font-style:italic;text-align:center;padding:1.5rem}' + . '.wp-block-post-title{text-align:center}' + . '</style>'; + + $h = $css . '<div class="fea-eed">'; + $h .= '<div class="fea-eed-head">' + . '<a class="fea-eed-nav" href="' . $href($prev) . '">‹ ' . esc_html($lbl($prev)) . '</a>' + . '<h2 class="fea-eed-date">' . esc_html($d . ' de ' . $MESES[$m]) . '</h2>' + . '<a class="fea-eed-nav" href="' . $href($next) . '">' . esc_html($lbl($next)) . ' ›</a>' + . '</div>'; + $h .= '<div class="fea-eed-tabs">' + . '<input type="radio" name="fed-tab" id="fed-texto" checked>' + . '<input type="radio" name="fed-tab" id="fed-video">' + . '<div class="fea-eed-labels">' + . '<label for="fed-texto">A la fuente cada día</label>' + . '<label for="fed-video">Otro evangelio es posible (vídeo)</label>' + . '</div>' + . '<div class="fea-eed-panel fea-eed-panel-texto">' . $render($texto, 'texto') . '</div>' + . '<div class="fea-eed-panel fea-eed-panel-video">' . $render($video, 'video') . '</div>' + . '</div></div>'; + return $h; +}); + // ── Shortcode: [fea_eucaristia] ─────────────────────────────────────────── // Fuente principal: sección "Para unas eucaristías más participativas" de la carta. // Fallback: cat 1648 por fecha. @@ -822,6 +1279,20 @@ add_shortcode('fea_multimedia', function($atts) { $labels = fea_labels(); $atts = shortcode_atts(['titulo' => $labels['multimedia']], $atts); $page = fea_front_page_id(); + $more_labels = [ + 'es' => 'Ver más', + 'en' => 'View all', + 'fr' => 'Voir plus', + 'it' => 'Vedi tutto', + 'pt' => 'Ver mais', + ]; + $more_label = $more_labels[$lang] ?? $more_labels['es']; + $index_id = 18977; + if ($lang !== 'es' && function_exists('pll_get_post')) { + $translated_index = (int) pll_get_post($index_id, $lang); + if ($translated_index) $index_id = $translated_index; + } + $index_url = get_permalink($index_id); $posts = function_exists('fea_carta_section_posts') ? fea_carta_section_posts('multimedia', $lang) : []; @@ -844,7 +1315,11 @@ add_shortcode('fea_multimedia', function($atts) { if (!$posts) return ''; $html = '<section class="fea-section">' + . '<div class="fea-section-head">' . '<h2 class="fea-section-title">' . esc_html($atts['titulo']) . '</h2>' + . ($index_url ? '<a class="fea-section-more" href="' . esc_url($index_url) . '">' + . esc_html($more_label) . ' <span aria-hidden="true">→</span></a>' : '') + . '</div>' . '<div class="fea-grid">'; foreach ($posts as $post) $html .= fea_card($post); return $html . '</div></section>'; @@ -882,6 +1357,10 @@ 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. + if (!is_singular()) return $content; return preg_replace_callback( '/<a\s([^>]*\s)?href=["\']([^"\']+)["\']([^>]*)>/i', @@ -1244,7 +1723,7 @@ add_shortcode('effa_seccion', function($atts) { $items[] = $cell; } - $html = '<table style="width:100%;border-collapse:collapse;table-layout:fixed;">'; + $html = '<table class="effa-proyecto-table" style="width:100%;border-collapse:collapse;table-layout:fixed;">'; foreach (array_chunk($items, 4) as $row) { while (count($row) < 4) $row[] = '<td></td>'; $html .= '<tr>' . implode('', $row) . '</tr>'; @@ -1282,7 +1761,7 @@ add_shortcode('effa_proyecto', function() { $items[] = $cell; } - $html = '<table style="width:100%;border-collapse:collapse;table-layout:fixed;">'; + $html = '<table class="effa-proyecto-table" style="width:100%;border-collapse:collapse;table-layout:fixed;">'; foreach (array_chunk($items, 4) as $row) { while (count($row) < 4) $row[] = '<td></td>'; $html .= '<tr>' . implode('', $row) . '</tr>'; @@ -1306,6 +1785,12 @@ add_action('wp_head', function() { .effa-cta-wrap { text-align: center; margin: 2rem 0 1rem; } .effa-cta { display: inline-block; padding: 0.6em 1.8em; background: #E89A1A; color: #fff; border-radius: 999px; text-decoration: none; font-weight: 700; font-size: 1rem; } .effa-cta:hover { background: #c97d10; color: #fff; } + /* Tarjetas EFFA: reflota de 4 columnas a 2 en movil */ + @media (max-width: 600px) { + .effa-proyecto-table tr { display: flex; flex-wrap: wrap; } + .effa-proyecto-table td { width: 50% !important; box-sizing: border-box; } + .effa-proyecto-table td:empty { display: none; } + } </style> <?php }, 20);