> $schedules Planifications existantes. * @return array> */ public static function register_cron_interval( array $schedules ): array { if ( ! isset( $schedules[ self::CRON_INTERVAL ] ) ) { $schedules[ self::CRON_INTERVAL ] = array( 'interval' => 2 * HOUR_IN_SECONDS, 'display' => 'Toutes les 2 heures (Actus Confédérales CGT)', ); } return $schedules; } /** * Planifie la tâche WP-Cron si elle n'est pas déjà active. */ public static function ensure_cron_scheduled(): void { if ( ! wp_next_scheduled( self::CRON_HOOK ) ) { wp_schedule_event( time() + 300, self::CRON_INTERVAL, self::CRON_HOOK ); } } /** * Supprime la tâche WP-Cron lors du changement de thème. */ public static function clear_cron(): void { $timestamp = wp_next_scheduled( self::CRON_HOOK ); while ( $timestamp ) { wp_unschedule_event( $timestamp, self::CRON_HOOK ); $timestamp = wp_next_scheduled( self::CRON_HOOK ); } } /** * Callback exécuté par WP-Cron toutes les 2 heures. */ public static function cron_refresh(): void { self::get_feed( true ); } /** * Bouton « Actualiser les actus confédérales » dans la barre d'admin WordPress. */ public static function add_admin_bar_button( WP_Admin_Bar $wp_admin_bar ): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $url = wp_nonce_url( admin_url( 'admin-post.php?action=cgt_fha_refresh_feed' ), 'cgt_fha_refresh_feed_action', 'cgt_fha_refresh_feed_nonce' ); $wp_admin_bar->add_node( array( 'id' => 'cgt-fha-refresh-confederal', 'title' => 'Actualiser les actus confédérales', 'href' => $url, 'meta' => array( 'title' => 'Synchroniser immédiatement les actualités depuis cgt.fr', ), ) ); } /** * Traitement du rafraîchissement manuel sécurisé par nonce. */ public static function handle_manual_refresh(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Action non autorisée.', 'cgt-fha' ), 403 ); } check_admin_referer( 'cgt_fha_refresh_feed_action', 'cgt_fha_refresh_feed_nonce' ); delete_transient( self::LOCK_TRANSIENT ); self::get_feed( true ); $redirect = add_query_arg( array( 'page' => 'cgt-fha-settings', 'tab' => 'confederal', 'cgt_feed_refreshed' => '1', ), admin_url( 'themes.php' ) ); wp_safe_redirect( $redirect ); exit; } /** * Retourne les données du flux confédéral (cache -> réseau -> backup -> fallback). * * @param bool $force_refresh Force une requête réseau immédiate. * @return array */ public static function get_feed( bool $force_refresh = false ): array { if ( ! $force_refresh ) { $cached = get_transient( self::TRANSIENT_KEY ); if ( is_array( $cached ) && ! empty( $cached['featured'] ) ) { return $cached; } } // Anti-tempête : évite de relancer des requêtes HTTP bloquantes si cgt.fr vient d'échouer. if ( $force_refresh || false === get_transient( self::LOCK_TRANSIENT ) ) { $fresh = self::fetch_and_parse(); if ( ! empty( $fresh['featured']['title'] ) && ! empty( $fresh['latest'] ) ) { set_transient( self::TRANSIENT_KEY, $fresh, 3 * HOUR_IN_SECONDS ); update_option( self::BACKUP_OPTION, $fresh, false ); delete_transient( self::LOCK_TRANSIENT ); return $fresh; } set_transient( self::LOCK_TRANSIENT, 1, 5 * MINUTE_IN_SECONDS ); } $backup = get_option( self::BACKUP_OPTION, array() ); if ( is_array( $backup ) && ! empty( $backup['featured']['title'] ) ) { return $backup; } return self::get_static_fallback(); } /** * Retourne l'état du cache pour l'onglet d'administration. * * @return array */ public static function get_cache_status(): array { $transient = get_transient( self::TRANSIENT_KEY ); $backup = get_option( self::BACKUP_OPTION, array() ); $data = is_array( $transient ) && ! empty( $transient ) ? $transient : ( is_array( $backup ) ? $backup : array() ); return array( 'has_transient' => is_array( $transient ) && ! empty( $transient ), 'fetched_at' => $data['fetched_at'] ?? 0, 'source' => $data['source'] ?? 'Fallback local', 'featured' => $data['featured'] ?? array(), ); } /** * Télécharge et parse cgt.fr (avec enrichissement sur /actualites puis sur la page article si besoin). * * @return array */ private static function fetch_and_parse(): array { $home_html = self::remote_get_html( 'https://www.cgt.fr/' ); $featured = array(); $latest = array(); $source = 'https://www.cgt.fr/'; if ( '' !== $home_html ) { $parsed = self::parse_homepage_html( $home_html ); $featured = $parsed['featured']; $latest = $parsed['latest']; } $needs_actualites = empty( $featured['title'] ) || '' === trim( (string) ( $featured['summary'] ?? '' ) ) || empty( $featured['image_url'] ) || count( $latest ) < 3; if ( ! $needs_actualites ) { foreach ( $latest as $item ) { if ( '' === trim( (string) ( $item['summary'] ?? '' ) ) || empty( $item['image_url'] ) ) { $needs_actualites = true; break; } } } if ( $needs_actualites ) { $actus_html = self::remote_get_html( 'https://www.cgt.fr/actualites' ); if ( '' !== $actus_html ) { $actus_items = self::parse_actualites_html( $actus_html ); if ( ! empty( $actus_items ) ) { $by_path = array(); foreach ( $actus_items as $act ) { $key = self::normalize_url_key( $act['url'] ); if ( '' !== $key ) { $by_path[ $key ] = $act; } } // Enrichit "featured" sans écraser une image haute définition déjà extraite. if ( ! empty( $featured['url'] ) ) { $f_key = self::normalize_url_key( $featured['url'] ); if ( isset( $by_path[ $f_key ] ) ) { if ( '' === trim( (string) ( $featured['summary'] ?? '' ) ) && ! empty( $by_path[ $f_key ]['summary'] ) ) { $featured['summary'] = $by_path[ $f_key ]['summary']; } if ( empty( $featured['image_url'] ) && ! empty( $by_path[ $f_key ]['image_url'] ) ) { $featured['image_url'] = $by_path[ $f_key ]['image_url']; } if ( ( empty( $featured['category'] ) || 'Confédération CGT' === $featured['category'] ) && ! empty( $by_path[ $f_key ]['category'] ) ) { $featured['category'] = $by_path[ $f_key ]['category']; } } } // Enrichit les 3 cartes "latest" depuis /actualites sans écraser image_a_la_une. foreach ( $latest as $idx => $item ) { $l_key = self::normalize_url_key( $item['url'] ); if ( isset( $by_path[ $l_key ] ) ) { if ( '' === trim( (string) ( $item['summary'] ?? '' ) ) && ! empty( $by_path[ $l_key ]['summary'] ) ) { $latest[ $idx ]['summary'] = $by_path[ $l_key ]['summary']; } if ( empty( $item['image_url'] ) && ! empty( $by_path[ $l_key ]['image_url'] ) ) { $latest[ $idx ]['image_url'] = $by_path[ $l_key ]['image_url']; } if ( ( empty( $item['category'] ) || 'Confédération CGT' === $item['category'] ) && ! empty( $by_path[ $l_key ]['category'] ) ) { $latest[ $idx ]['category'] = $by_path[ $l_key ]['category']; } } } // Fallback complet si la page d'accueil a changé de structure. if ( empty( $featured['title'] ) && count( $actus_items ) >= 2 ) { $featured = array_shift( $actus_items ); $latest = array_slice( $actus_items, 0, 3 ); $source = 'https://www.cgt.fr/actualites (fallback)'; } elseif ( count( $latest ) < 3 ) { $seen = array( self::normalize_url_key( (string) ( $featured['url'] ?? '' ) ) ); foreach ( $latest as $l ) { $seen[] = self::normalize_url_key( (string) $l['url'] ); } foreach ( $actus_items as $act ) { if ( count( $latest ) >= 3 ) { break; } $act_key = self::normalize_url_key( $act['url'] ); if ( '' !== $act_key && ! in_array( $act_key, $seen, true ) ) { $latest[] = $act; $seen[] = $act_key; } } } } } } if ( empty( $featured['title'] ) || empty( $latest ) ) { return array(); } // Fallback final : complète un éventuel article épinglé absent de la page 1 d'/actualites. if ( '' === trim( (string) ( $featured['summary'] ?? '' ) ) || empty( $featured['image_url'] ) ) { $featured = self::enrich_from_article_page( $featured ); } foreach ( $latest as $idx => $item ) { if ( '' === trim( (string) ( $item['summary'] ?? '' ) ) || empty( $item['image_url'] ) ) { $latest[ $idx ] = self::enrich_from_article_page( $item ); } } return array( 'fetched_at' => time(), 'source' => $source, 'featured' => self::normalize_article( $featured, 240 ), 'latest' => array_map( static fn( array $item ): array => self::normalize_article( $item, 190 ), array_slice( $latest, 0, 3 ) ), ); } /** * Requête HTTP GET sécurisée avec timeout 10s. */ private static function remote_get_html( string $url ): string { $response = wp_remote_get( $url, array( 'timeout' => 10, 'redirection' => 3, 'user-agent' => 'Mozilla/5.0 (compatible; CGT-FHA-Theme/1.1; +https://cgt-fha.fr/)', 'headers' => array( 'Accept' => 'text/html,application/xhtml+xml', 'Accept-Language' => 'fr-FR,fr;q=0.9', ), ) ); if ( is_wp_error( $response ) ) { return ''; } $code = (int) wp_remote_retrieve_response_code( $response ); if ( $code < 200 || $code >= 400 ) { return ''; } return (string) wp_remote_retrieve_body( $response ); } /** * Parse la page d'accueil cgt.fr : * - "À la une de la CGT" (#block-bluedrop-views-block-derniers-ajouts-block-2, .relative-wrapper) * - "mais aussi..." (#block-views-block-derniers-ajouts-block-1, .views-row) * * @return array{featured: array, latest: array>} */ private static function parse_homepage_html( string $html ): array { $xpath = self::create_xpath( $html ); if ( ! $xpath ) { return array( 'featured' => array(), 'latest' => array() ); } $featured = array(); $latest = array(); // 1. Bloc « à la une de la cgt » (ID Drupal exact + secours par titre de section). $une_root = null; $une_nodes = $xpath->query( '//section[@id="block-bluedrop-views-block-derniers-ajouts-block-2"]' ); if ( $une_nodes instanceof DOMNodeList && $une_nodes->length > 0 ) { $une_root = $une_nodes->item( 0 ); } else { $une_root = self::find_section_by_heading_text( $xpath, 'la une de la cgt' ); } if ( $une_root ) { $wrappers = $xpath->query( './/div[contains(@class,"relative-wrapper")]', $une_root ); $card_el = ( $wrappers instanceof DOMNodeList && $wrappers->length > 0 ) ? $wrappers->item( 0 ) : $une_root; $parsed = self::extract_drupal_row( $xpath, $card_el ); if ( ! empty( $parsed['title'] ) && ! empty( $parsed['url'] ) ) { $featured = $parsed; } } // 2. Bloc « mais aussi... » (ID Drupal exact + secours par titre de section). $mais_root = null; $mais_nodes = $xpath->query( '//section[@id="block-views-block-derniers-ajouts-block-1"]' ); if ( $mais_nodes instanceof DOMNodeList && $mais_nodes->length > 0 ) { $mais_root = $mais_nodes->item( 0 ); } else { $mais_root = self::find_section_by_heading_text( $xpath, 'mais aussi' ); } if ( $mais_root ) { $rows = $xpath->query( './/div[contains(@class,"views-row")]', $mais_root ); if ( $rows instanceof DOMNodeList ) { $seen = array( self::normalize_url_key( (string) ( $featured['url'] ?? '' ) ) ); foreach ( $rows as $row ) { if ( count( $latest ) >= 3 ) { break; } $item = self::extract_drupal_row( $xpath, $row ); $key = self::normalize_url_key( (string) ( $item['url'] ?? '' ) ); if ( ! empty( $item['title'] ) && '' !== $key && ! in_array( $key, $seen, true ) ) { $latest[] = $item; $seen[] = $key; } } } } return array( 'featured' => $featured, 'latest' => $latest, ); } /** * Parse la liste des articles de https://www.cgt.fr/actualites (jusqu'à 25 lignes .views-row). * * @return array> */ private static function parse_actualites_html( string $html ): array { $xpath = self::create_xpath( $html ); if ( ! $xpath || ! $xpath->document->documentElement ) { return array(); } $rows = $xpath->query( '//main//div[contains(@class,"views-row")]|//div[contains(@class,"view-content")]//div[contains(@class,"views-row")]' ); if ( ! ( $rows instanceof DOMNodeList ) || 0 === $rows->length ) { return array(); } $articles = array(); $seen = array(); foreach ( $rows as $row ) { if ( count( $articles ) >= 25 ) { break; } $item = self::extract_drupal_row( $xpath, $row ); $key = self::normalize_url_key( (string) ( $item['url'] ?? '' ) ); if ( ! empty( $item['title'] ) && '' !== $key && ! isset( $seen[ $key ] ) ) { $seen[ $key ] = true; $articles[] = $item; } } return $articles; } /** * Extrait proprement un article depuis un conteneur Drupal (bloc À la une ou .views-row). * * @return array */ private static function extract_drupal_row( DOMXPath $xpath, DOMNode $row ): array { $link_el = null; $h_links = $xpath->query( './/h3//a[@href]|.//h2//a[@href]|.//h4//a[@href]', $row ); if ( $h_links instanceof DOMNodeList ) { foreach ( $h_links as $cand ) { $href = trim( $cand->getAttribute( 'href' ) ); if ( self::is_article_href( $href ) ) { $link_el = $cand; break; } } } if ( ! $link_el ) { $fallback_links = $xpath->query( './/a[@href and not(contains(@href,"/terms/")) and not(contains(@href,"/taxonomy/")) and not(contains(@class,"taxonomy-style")) and not(contains(@class,"more-link"))]', $row ); if ( $fallback_links instanceof DOMNodeList ) { foreach ( $fallback_links as $cand ) { $href = trim( $cand->getAttribute( 'href' ) ); if ( self::is_article_href( $href ) ) { $link_el = $cand; break; } } } } if ( ! $link_el ) { return array(); } $url = self::absolutize_url( $link_el->getAttribute( 'href' ) ); $title = self::normalize_space( $link_el->textContent ); if ( '' === $url || strlen( $title ) < 6 ) { return array(); } // Catégories (.taxonomy-style hors badge .highlighted). $cats = array(); $cat_nodes = $xpath->query( './/*[contains(@class,"taxonomy-style")]', $row ); if ( $cat_nodes instanceof DOMNodeList ) { foreach ( $cat_nodes as $cat_node ) { if ( $cat_node instanceof DOMElement && false !== strpos( $cat_node->getAttribute( 'class' ), 'highlighted' ) ) { continue; } $cat_txt = self::normalize_space( $cat_node->textContent ); if ( '' !== $cat_txt && strlen( $cat_txt ) < 45 && ! in_array( $cat_txt, $cats, true ) ) { $cats[] = $cat_txt; } } } $category = ! empty( $cats ) ? implode( ' | ', array_slice( $cats, 0, 2 ) ) : 'Confédération CGT'; // Résumé (.field-type-text-long ou paragraphe dans .txt-container). $summary = ''; $p_nodes = $xpath->query( './/*[contains(@class,"field-type-text-long")]|.//div[contains(@class,"txt-container")]//p|.//p', $row ); if ( $p_nodes instanceof DOMNodeList ) { foreach ( $p_nodes as $p ) { $txt = self::normalize_space( $p->textContent ); if ( strlen( $txt ) >= 10 && $txt !== $title ) { $summary = $txt; break; } } } // Image (.views-field-field-imagette-media img en priorité, sinon premier img). $image_url = ''; $img_nodes = $xpath->query( './/div[contains(@class,"views-field-field-imagette-media")]//img|.//img', $row ); if ( $img_nodes instanceof DOMNodeList && $img_nodes->length > 0 ) { $img = $img_nodes->item( 0 ); if ( $img instanceof DOMElement ) { $src = $img->getAttribute( 'data-src' ) ?: $img->getAttribute( 'src' ); if ( '' === $src && $img->hasAttribute( 'srcset' ) ) { $parts = explode( ',', $img->getAttribute( 'srcset' ) ); $first = trim( (string) $parts[0] ); $src = strtok( $first, ' ' ) ?: ''; } $image_url = self::absolutize_url( $src ); } } return array( 'title' => $title, 'summary' => $summary, 'image_url' => $image_url, 'category' => $category, 'url' => $url, ); } /** * Enrichit un article directement depuis sa page individuelle sur cgt.fr si résumé ou image manquent. * * @param array $item Article partiel. * @return array */ private static function enrich_from_article_page( array $item ): array { if ( empty( $item['url'] ) ) { return $item; } $html = self::remote_get_html( $item['url'] ); if ( '' === $html ) { return $item; } $xpath = self::create_xpath( $html ); if ( ! $xpath ) { return $item; } if ( '' === trim( (string) ( $item['summary'] ?? '' ) ) ) { $candidates = array( './/*[contains(@class,"chapo") or contains(@class,"field-name-field-chapo")]', '//meta[@property="og:description"]/@content', '//meta[@name="description"]/@content', '//main//article//p', ); foreach ( $candidates as $query ) { $nodes = $xpath->query( $query ); if ( $nodes instanceof DOMNodeList && $nodes->length > 0 ) { foreach ( $nodes as $node ) { $txt = self::normalize_space( $node instanceof DOMAttr ? $node->value : $node->textContent ); if ( strlen( $txt ) >= 15 && $txt !== ( $item['title'] ?? '' ) ) { $item['summary'] = $txt; break 2; } } } } } if ( empty( $item['image_url'] ) ) { $og_img = $xpath->query( '//meta[@property="og:image"]/@content' ); if ( $og_img instanceof DOMNodeList && $og_img->length > 0 ) { $src = trim( (string) $og_img->item( 0 )->nodeValue ); if ( '' !== $src ) { $item['image_url'] = self::absolutize_url( $src ); } } } return $item; } /** * Localise la section ancêtre correspondant à un titre textuel donné. */ private static function find_section_by_heading_text( DOMXPath $xpath, string $needle ): ?DOMNode { $headings = $xpath->query( '//h1|//h2|//h3' ); if ( ! ( $headings instanceof DOMNodeList ) ) { return null; } foreach ( $headings as $heading ) { $text = self::normalize_space( $heading->textContent ); $low = function_exists( 'mb_strtolower' ) ? mb_strtolower( $text, 'UTF-8' ) : strtolower( $text ); if ( false !== strpos( $low, $needle ) ) { $cur = $heading->parentNode; while ( $cur && $cur instanceof DOMElement ) { if ( 'section' === strtolower( $cur->tagName ) ) { return $cur; } $cur = $cur->parentNode; } return $heading->parentNode; } } return null; } /** * Vérifie qu'un href pointe vers un article (et non une taxonomie ou l'index /actualites). */ private static function is_article_href( string $href ): bool { if ( '' === $href || '#' === $href || 0 === strpos( $href, 'mailto:' ) || 0 === strpos( $href, 'tel:' ) ) { return false; } if ( false !== strpos( $href, '/terms/' ) || false !== strpos( $href, '/taxonomy/' ) ) { return false; } $path = untrailingslashit( (string) wp_parse_url( $href, PHP_URL_PATH ) ); return '' !== $path && '/actualites' !== $path && '/' !== $path; } /** * Normalise une URL sur son chemin pour la correspondance fiable entre accueil et /actualites. */ private static function normalize_url_key( string $url ): string { $path = (string) wp_parse_url( trim( $url ), PHP_URL_PATH ); $path = strtolower( untrailingslashit( $path ) ); return '' === $path || '/' === $path ? '' : $path; } /** * Initialise un DOMXPath UTF-8 fiable sur PHP 8.x sans altérer les accents ni apostrophes typographiques. */ private static function create_xpath( string $html ): ?DOMXPath { if ( '' === trim( $html ) || ! class_exists( 'DOMDocument' ) ) { return null; } if ( function_exists( 'mb_encode_numericentity' ) ) { $prepared = mb_encode_numericentity( $html, array( 0x80, 0x10FFFF, 0, ~0 ), 'UTF-8' ); } else { $prepared = preg_replace( '/]*>/i', '$0', $html, 1 ) ?: $html; } $prev = libxml_use_internal_errors( true ); $dom = new DOMDocument( '1.0', 'UTF-8' ); $dom->loadHTML( $prepared, LIBXML_NONET | LIBXML_NOWARNING | LIBXML_NOERROR ); libxml_clear_errors(); libxml_use_internal_errors( $prev ); return new DOMXPath( $dom ); } /** * Convertit une URL relative cgt.fr en URL absolue. */ private static function absolutize_url( string $raw ): string { $raw = trim( $raw ); if ( '' === $raw || 0 === strpos( $raw, 'data:' ) || 0 === strpos( $raw, 'javascript:' ) ) { return ''; } if ( 0 === strpos( $raw, '//' ) ) { return 'https:' . $raw; } if ( preg_match( '#^https?://#i', $raw ) ) { return esc_url_raw( $raw ); } if ( '/' === $raw[0] ) { return esc_url_raw( 'https://www.cgt.fr' . $raw ); } return esc_url_raw( 'https://www.cgt.fr/' . ltrim( $raw, '/' ) ); } /** * Normalise les espaces blancs et entités HTML. */ private static function normalize_space( string $text ): string { $decoded = html_entity_decode( wp_strip_all_tags( $text ), ENT_QUOTES | ENT_HTML5, 'UTF-8' ); return trim( (string) preg_replace( '/\s+/u', ' ', $decoded ) ); } /** * Nettoie et tronque un article pour l'affichage sans phrase générique répétitive. * * @param array $item Données brutes de l'article. * @return array */ private static function normalize_article( array $item, int $max_chars = 190 ): array { $placeholder = CGT_FHA_URI . '/assets/img/placeholder.svg'; $summary = self::normalize_space( (string) ( $item['summary'] ?? '' ) ); if ( '' !== $summary && function_exists( 'mb_strlen' ) && mb_strlen( $summary, 'UTF-8' ) > $max_chars ) { $summary = rtrim( mb_substr( $summary, 0, $max_chars - 1, 'UTF-8' ) ) . '…'; } return array( 'title' => self::normalize_space( (string) ( $item['title'] ?? '' ) ), 'summary' => $summary, 'image_url' => ! empty( $item['image_url'] ) ? esc_url_raw( (string) $item['image_url'] ) : $placeholder, 'category' => self::normalize_space( (string) ( $item['category'] ?? 'Confédération CGT' ) ), 'url' => ! empty( $item['url'] ) ? esc_url_raw( (string) $item['url'] ) : 'https://www.cgt.fr/actualites', ); } /** * Fallback statique si cgt.fr est inaccessible lors du premier démarrage. * * @return array */ private static function get_static_fallback(): array { $placeholder = CGT_FHA_URI . '/assets/img/placeholder.svg'; return array( 'fetched_at' => 0, 'source' => 'Fallback éditorial local', 'featured' => array( 'title' => 'Salaires, retraites, services publics : toutes et tous mobilisés avec la CGT', 'summary' => 'Retrouvez les dossiers nationaux, les revendications confédérales et les appels à l\'action portés par la CGT partout en France.', 'image_url' => $placeholder, 'category' => 'À la une', 'url' => 'https://www.cgt.fr/actualites', ), 'latest' => array( array( 'title' => 'Pouvoir d\'achat : augmenter les salaires et le SMIC', 'summary' => 'Les propositions économiques de la CGT pour indexer les salaires sur l\'inflation et revaloriser les grilles.', 'image_url' => $placeholder, 'category' => 'Salaires', 'url' => 'https://www.cgt.fr/actualites', ), array( 'title' => 'Conditions de travail & santé au travail', 'summary' => 'Prévenir l\'usure professionnelle, renforcer le rôle des élus CSE et défendre les droits des salarié·e·s.', 'image_url' => $placeholder, 'category' => 'Travail', 'url' => 'https://www.cgt.fr/actualites', ), array( 'title' => 'Services publics de proximité dans nos territoires', 'summary' => 'Défendre l\'hôpital public, l\'école, les transports et les services publics essentiels en milieu rural.', 'image_url' => $placeholder, 'category' => 'Services publics', 'url' => 'https://www.cgt.fr/actualites', ), ), ); } /** * Rendu HTML/CSS de la Section 4 (fidèle à la maquette). */ public static function render_section( string $section_title = 'ACTUALITÉ CONFÉDÉRALE' ): void { $feed = self::get_feed(); $featured = $feed['featured'] ?? array(); $latest = $feed['latest'] ?? array(); if ( empty( $featured ) ) { return; } ?>