is_main_query()) return;
if (!function_exists('pll_current_language') || !function_exists('pll_get_post')) return;
// Solo actuar cuando WP cree que está en el home/front page
if (!$query->is_home() && !$query->is_front_page()) return;
$lang = pll_current_language();
if (!$lang || $lang === 'es') return;
$translated_id = pll_get_post(26542, $lang);
if (!$translated_id) return;
// Forzar carga de la página traducida como si fuera la portada estática
$query->set('page_id', $translated_id);
$query->set('post_type', 'page');
$query->is_home = false;
$query->is_front_page = true;
$query->is_singular = true;
$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);
// 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);
// /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) {
if (!function_exists('pll_current_language') || !function_exists('pll_get_post')) return $value;
static $running = false;
if ($running) return $value; // evitar recursión
$running = true;
$lang = pll_current_language();
$running = false;
if (!$lang || $lang === 'es') return $value;
$translated = pll_get_post((int) $value, $lang);
return $translated ?: $value;
});
// ── Traducir links de categoría "Esta semana" / "Semana pasada" al idioma actual ─
// Las categorías traducidas EN/FR/IT/PT dan 404 (bug WP_Tax_Query AND 0=1 con Polylang).
// Solución: redirigir ES cat URL → post traducido directamente (no a la categoría).
// Para "Otras semanas" (cartas-de-otras-semanas, count=729): se deja como está.
add_action('wp_footer', function() {
if (!function_exists('pll_current_language') || !function_exists('pll_get_post')) return;
$lang = pll_current_language();
if (!$lang || $lang === 'es') return;
global $wpdb;
$map = [];
// Categorías de carta única (count=1) → redirigir al post traducido
$single_carta_cats = [
'cartasemana' => 6,
'carta-semana-pasada' => 22,
];
foreach ($single_carta_cats as $slug => $es_cat_id) {
// term_taxonomy_id de la categoría ES (bypassa Polylang)
$tt_id = (int) $wpdb->get_var($wpdb->prepare(
"SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy}
WHERE term_id=%d AND taxonomy='category'", $es_cat_id
));
if (!$tt_id) continue;
// Post ES más reciente en esa categoría
$es_post_id = (int) $wpdb->get_var($wpdb->prepare(
"SELECT p.ID FROM {$wpdb->posts} p
JOIN {$wpdb->term_relationships} tr ON p.ID=tr.object_id
WHERE tr.term_taxonomy_id=%d AND p.post_status='publish'
ORDER BY p.post_date DESC LIMIT 1", $tt_id
));
if (!$es_post_id) continue;
// Post traducido
$trans_id = pll_get_post($es_post_id, $lang);
if (!$trans_id) continue;
$trans_url = get_permalink($trans_id);
if ($trans_url) {
$map[home_url('/category/' . $slug . '/')] = $trans_url;
}
}
if (empty($map)) return;
?>
'group_user_foto_perfil',
'title' => 'Foto de perfil',
'fields' => [[
'key' => 'field_user_foto_perfil',
'label' => 'Foto',
'name' => 'foto_perfil',
'type' => 'image',
'instructions' => 'Sube una foto cuadrada del autor (mínimo 100×100px).',
'return_format' => 'url',
'preview_size' => 'thumbnail',
'upload_folder' => 'autores',
]],
'location' => [[
['param' => 'user_form', 'operator' => '==', 'value' => 'all'],
]],
]);
});
// Usar la foto del autor en lugar del Gravatar cuando existe
// Lee el attachment ID guardado en el meta 'foto_perfil' (campo ACF)
add_filter('get_avatar_url', function($url, $id_or_email, $args) {
$user_id = null;
if (is_numeric($id_or_email)) $user_id = (int) $id_or_email;
elseif ($id_or_email instanceof WP_User) $user_id = $id_or_email->ID;
elseif (is_string($id_or_email)) {
$user = get_user_by('email', $id_or_email);
if ($user) $user_id = $user->ID;
}
if (!$user_id) return $url;
$attach_id = get_user_meta($user_id, 'foto_perfil', true);
if ($attach_id) {
$foto = wp_get_attachment_image_url((int) $attach_id, 'full');
if ($foto) return $foto;
}
return $url;
}, 10, 3);
// ── Ordenar por fecha los resultados del buscador ACF en campos de portada ─
add_filter('acf/fields/relationship/query/key=field_portada_articulos', function($args) {
$args['orderby'] = 'date';
$args['order'] = 'DESC';
return $args;
});
add_filter('acf/fields/relationship/query/key=field_portada_multimedia', function($args) {
$args['orderby'] = 'date';
$args['order'] = 'DESC';
return $args;
});
// ── Campos ACF para la portada ────────────────────────────────────────────
add_action('acf/init', function() {
if (!function_exists('acf_add_local_field_group')) return;
$front_page_id = (int) get_option('page_on_front');
acf_add_local_field_group([
'key' => 'group_portada_fea',
'title' => 'Contenido de la portada',
'fields' => [
[
'key' => 'field_portada_articulos',
'label' => 'Artículos seleccionados',
'name' => 'portada_articulos',
'type' => 'relationship',
'instructions' => 'Elige los artículos que aparecerán en la portada esta semana (máx. 9). Puedes buscar por título.',
'post_type' => ['post'],
'post_status' => ['publish', 'draft'],
'filters' => ['search', 'taxonomy'],
'elements' => ['featured_image'],
'min' => 0,
'max' => 9,
'return_format' => 'object',
'query_args' => ['orderby' => 'date', 'order' => 'DESC'],
],
[
'key' => 'field_portada_multimedia',
'label' => 'Multimedia seleccionado',
'name' => 'portada_multimedia',
'type' => 'relationship',
'instructions' => 'Elige los vídeos o audios para la portada (máx. 4).',
'post_type' => ['post'],
'post_status' => ['publish', 'draft'],
'filters' => ['search'],
'elements' => ['featured_image'],
'min' => 0,
'max' => 4,
'return_format' => 'object',
'query_args' => ['orderby' => 'date', 'order' => 'DESC'],
],
],
'location' => [[
['param' => 'page', 'operator' => '==', 'value' => (string) $front_page_id],
]],
'position' => 'normal',
'style' => 'default',
'label_placement' => 'top',
]);
});
// ── Selector de idioma (dropdown con bandera + 2 letras) ─────────────────
add_action('wp_head', function() {
if (!function_exists('pll_the_languages')) return;
?>
'🇪🇸', 'en' => '🇬🇧', 'fr' => '🇫🇷', 'it' => '🇮🇹', 'pt' => '🇵🇹'];
$langs = pll_the_languages(['raw' => 1, 'hide_if_no_translation' => 0]);
if (!$langs) return;
$current = array_filter($langs, fn($l) => $l['current_lang']);
$current = $current ? array_values($current)[0] : null;
$cur_slug = $current ? $current['slug'] : 'es';
$cur_flag = $flags[$cur_slug] ?? '🌐';
$cur_code = strtoupper($cur_slug);
$items = '';
foreach ($langs as $l) {
$flag = $flags[$l['slug']] ?? '🌐';
$code = strtoupper($l['slug']);
$cur = $l['current_lang'] ? ' aria-current="true"' : '';
// 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 .= '
';
});
add_action('wp_head', function() {
if (!is_single()) return;
?>
';
}, 10, 2);
// ── Helpers ───────────────────────────────────────────────────────────────
function fea_title(string $title): string {
$lower = mb_strtolower($title, '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 '';
}
/** Devuelve 'antiguo-testamento' / 'nuevo-testamento' si el texto es esa firma, o ''. #66 */
function fea_testamento_de_texto(string $txt): string {
$t = mb_strtolower(trim($txt), 'UTF-8');
if ($t === 'antiguo testamento') return 'antiguo-testamento';
if ($t === 'nuevo testamento') return 'nuevo-testamento';
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;
// Las traducciones tienen el título en su idioma (Matteo, Geremia, Romani…) que no
// reconocemos. Usamos el título del ORIGINAL ES, donde sí detectamos el libro → la
// traducción hereda el mismo símbolo que el ES (#135).
if (function_exists('pll_get_post_language') && pll_get_post_language($post->ID) !== 'es') {
$es = function_exists('pll_get_post') ? pll_get_post($post->ID, 'es') : 0;
if ($es) {
$esp = get_post($es);
if ($esp) $title = (string) $esp->post_title;
}
}
if ($ev = fea_evangelista_de_texto($title)) return $base . $ev . '.svg';
// "Antiguo Testamento" / "Nuevo Testamento" llevan símbolo propio (no el genérico). #66
if ($t = fea_testamento_de_texto($title) ?: fea_testamento_de_texto($author_name)) return $base . $t . '.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 = fea_avatar_url($post, 96, (int) $author_id, (string) $author_name);
$url = get_permalink($post->ID);
$title = fea_title($post->post_title);
return ''
. ''
. ''
. ''
. ''
. ''
. '
'
. '';
}
/** IDs de las páginas de portada en todos los idiomas. */
function fea_front_page_ids(): array {
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). */
function fea_is_front_page(): bool {
if (is_front_page()) return true;
$id = get_queried_object_id();
return $id && in_array($id, fea_front_page_ids(), true);
}
/** True para la landing de Escuela, que no muestra título visible. */
function fea_is_escuela_page(): bool {
if (!is_page()) return false;
$page = get_queried_object();
return $page instanceof WP_Post && $page->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';
}
/** Etiquetas de sección traducidas por idioma. */
function fea_labels(): array {
$all = [
'es' => [
'carta' => 'Carta de la semana',
'evangelio' => 'Comentarios al evangelio',
'articulos' => 'Artículos de esta semana',
'eucaristia' => 'Para una eucaristía más participativa',
'multimedia' => 'Multimedia',
],
'en' => [
'carta' => 'Letter of the week',
'evangelio' => 'Gospel commentary',
'articulos' => 'Articles of the week',
'eucaristia' => 'For a more participatory Eucharist',
'multimedia' => 'Multimedia',
],
'fr' => [
'carta' => 'Lettre de la semaine',
'evangelio' => 'Commentaires de l\'évangile',
'articulos' => 'Articles de la semaine',
'eucaristia' => 'Pour une eucharistie plus participative',
'multimedia' => 'Multimédia',
],
'it' => [
'carta' => 'Lettera della settimana',
'evangelio' => 'Commenti al vangelo',
'articulos' => 'Articoli della settimana',
'eucaristia' => 'Per una eucaristia più partecipativa',
'multimedia' => 'Multimedia',
],
'pt' => [
'carta' => 'Carta da semana',
'evangelio' => 'Comentários ao evangelho',
'articulos' => 'Artigos da semana',
'eucaristia' => 'Para uma eucaristia mais participativa',
'multimedia' => 'Multimédia',
],
];
return $all[fea_current_lang()] ?? $all['es'];
}
/**
* ID de la página de portada para el idioma actual.
*/
function fea_front_page_id(): int {
$es_front = 26542;
$lang = fea_current_lang();
if ($lang === 'es') return $es_front;
if (function_exists('pll_get_post')) {
$translated = pll_get_post($es_front, $lang);
if ($translated) return (int) $translated;
}
return $es_front;
}
/**
* Traduce un term_id de categoría ES al idioma actual.
* Si no hay traducción disponible, devuelve el ID original.
*/
function fea_cat(int $es_cat_id): int {
$lang = fea_current_lang();
if ($lang === 'es') return $es_cat_id;
if (function_exists('pll_get_term')) {
$translated = pll_get_term($es_cat_id, $lang);
if ($translated) return (int) $translated;
}
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 = '';
return $css . '
';
});
// ── 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 '
No hay ' . ($tipo === 'video' ? 'vídeo' : 'texto') . ' disponible para este día.