fix(portada): artículos por idioma y enlace evangelio del día (#132)

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 08:30:18 -04:00
parent 66ee943da4
commit 9ac857b027
2 changed files with 562 additions and 45 deletions
@@ -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/<k2id>-...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;
}
+514 -29
View File
@@ -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 <title>/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">&rsaquo;</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) . '">&lsaquo; ' . 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)) . ' &rsaquo;</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);