"https://schema.org", "@type" => "WebSite", "name" => SITE_NAME, "description" => SITE_DESCRIPTION, "url" => $baseUrl, "potentialAction" => [ "@type" => "SearchAction", "target" => [ "@type" => "EntryPoint", "urlTemplate" => $baseUrl . "/recherche.php?q={search_term_string}" ], "query-input" => "required name=search_term_string" ], "publisher" => [ "@type" => "Organization", "name" => ORGANIZATION_NAME, "url" => $baseUrl, "logo" => [ "@type" => "ImageObject", "url" => $baseUrl . "/img/logo.png" ] ] ]; return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } /** * Génère le JSON-LD pour un objet VideoObject * * @param array $videoData Données de la vidéo depuis l'API PeerTube * @param array $video Données formatées de la vidéo * @return string JSON-LD pour la vidéo */ function generateVideoObjectJsonLd($videoData, $video) { $baseUrl = getBaseUrl(); $videoUrl = $baseUrl . "/video.php?id=" . $video['id']; // Construire l'URL de la vignette $thumbnailUrl = isset($videoData['thumbnailPath']) ? PEERTUBE_URL . $videoData['thumbnailPath'] : $baseUrl . "/img/default-thumbnail.jpg"; // Formater la durée en format ISO 8601 (PT1H30M pour 1h30min) $duration = formatDurationISO8601($video['duration'] ?? 0); // Construire les données de base $data = [ "@context" => "https://schema.org", "@type" => "VideoObject", "name" => $video['title'], "description" => !empty($video['description']) ? truncateText(strip_tags($video['description']), 300) : "Regardez cette vidéo sur " . SITE_NAME, "url" => $videoUrl, "embedUrl" => PEERTUBE_URL . "/videos/embed/" . $video['id'], "inLanguage" => "fr-FR", "thumbnailUrl" => $thumbnailUrl, "uploadDate" => formatDateISO8601($video['date']), "duration" => $duration, "publisher" => [ "@type" => "Organization", "name" => SITE_NAME, "url" => $baseUrl, "logo" => [ "@type" => "ImageObject", "url" => $baseUrl . "/img/logo.png" ] ] ]; // Ajouter les informations de la chaîne/créateur if (!empty($video['channel'])) { $data["creator"] = [ "@type" => "Person", "name" => $video['channel'], "url" => PEERTUBE_URL . "/c/" . ($videoData['channel']['name'] ?? '') ]; } // Ajouter les statistiques d'interaction if (isset($video['views']) && $video['views'] > 0) { $data["interactionStatistic"] = [ "@type" => "InteractionCounter", "interactionType" => [ "@type" => "WatchAction" ], "userInteractionCount" => $video['views'] ]; } // Ajouter les likes si disponibles if (isset($video['likes']) && $video['likes'] > 0) { if (!isset($data["interactionStatistic"])) { $data["interactionStatistic"] = []; } else { // Convertir en array si c'était un seul élément if (isset($data["interactionStatistic"]["@type"])) { $data["interactionStatistic"] = [$data["interactionStatistic"]]; } } $data["interactionStatistic"][] = [ "@type" => "InteractionCounter", "interactionType" => [ "@type" => "LikeAction" ], "userInteractionCount" => $video['likes'] ]; } // Ajouter les tags/mots-clés si disponibles if (!empty($video['tags'])) { $data["keywords"] = implode(", ", $video['tags']); } // Ajouter les informations de licence si disponibles if (isset($videoData['licence']) && !empty($videoData['licence'])) { $licenceId = $videoData['licence']['id']; $licenceMapping = [ 1 => "https://creativecommons.org/licenses/by/4.0/", 2 => "https://creativecommons.org/licenses/by-sa/4.0/", 3 => "https://creativecommons.org/licenses/by-nd/4.0/", 4 => "https://creativecommons.org/licenses/by-nc/4.0/", 5 => "https://creativecommons.org/licenses/by-nc-sa/4.0/", 6 => "https://creativecommons.org/licenses/by-nc-nd/4.0/", 7 => "https://creativecommons.org/publicdomain/zero/1.0/" ]; if (isset($licenceMapping[$licenceId])) { $data["license"] = $licenceMapping[$licenceId]; } } // Ajouter la catégorie si disponible if (isset($videoData['category']) && !empty($videoData['category'])) { $data["genre"] = $videoData['category']['label'] ?? 'Vidéo'; } // Ajouter les dimensions si c'est un short (format portrait) if (isset($video['aspectRatio']) && $video['aspectRatio'] <= 1) { $data["videoFrameSize"] = "Portrait"; } return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } /** * Génère le JSON-LD pour un podcast (PodcastSeries + PodcastEpisode + AudioObject) * * @param array $episodes Épisodes formatés par getCastopodEpisodes() * @return string JSON-LD pour le podcast, chaîne vide si aucun épisode */ function generatePodcastJsonLd($episodes) { if (empty($episodes)) { return ''; } $baseUrl = getBaseUrl(); $first = $episodes[0]; // Informations de la série, déduites du flux RSS $seriesName = !empty($first['podcastTitle']) ? $first['podcastTitle'] : SITE_NAME; $seriesUrl = !empty($first['podcastLink']) ? $first['podcastLink'] : $baseUrl; $seriesImage = !empty($first['image']) ? $first['image'] : $baseUrl . '/img/logo.png'; $webFeed = !empty($first['podcastSlug']) ? rtrim(CASTOPOD_URL, '/') . '/@' . $first['podcastSlug'] . '/feed' : null; $series = [ "@type" => "PodcastSeries", "@id" => $seriesUrl . '#podcast', "name" => $seriesName, "url" => $seriesUrl, "image" => $seriesImage, "inLanguage" => "fr-FR", "author" => [ "@type" => "Organization", "name" => ORGANIZATION_NAME, "url" => $baseUrl ], "publisher" => [ "@type" => "Organization", "name" => SITE_NAME, "url" => $baseUrl, "logo" => [ "@type" => "ImageObject", "url" => $baseUrl . "/img/logo.png" ] ] ]; if ($webFeed) { $series["webFeed"] = $webFeed; } $episodeItems = []; foreach ($episodes as $episode) { $item = [ "@type" => "PodcastEpisode", "name" => $episode['title'], "url" => $episode['link'], "partOfSeries" => ["@id" => $seriesUrl . '#podcast'] ]; if (!empty($episode['pubDate'])) { $item["datePublished"] = formatDateISO8601($episode['pubDate']); } if (!empty($episode['image'])) { $item["image"] = $episode['image']; } if (!empty($episode['description'])) { $item["description"] = truncateText($episode['description'], 300); } $seconds = parseDurationToSeconds($episode['duration'] ?? ''); if ($seconds > 0) { $item["timeRequired"] = formatDurationISO8601($seconds); } if (!empty($episode['audioUrl'])) { $audio = [ "@type" => "AudioObject", "contentUrl" => $episode['audioUrl'] ]; $format = guessAudioEncodingFormat($episode['audioUrl']); if ($format !== null) { $audio["encodingFormat"] = $format; } $item["associatedMedia"] = $audio; } $episodeItems[] = $item; } $data = [ "@context" => "https://schema.org", "@graph" => array_merge([$series], $episodeItems) ]; return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } /** * Convertit une durée iTunes en secondes * Formats acceptés : "3600", "90", "MM:SS", "HH:MM:SS" * * @param string $duration Durée brute issue du flux RSS * @return int Durée en secondes (0 si non parsable) */ function parseDurationToSeconds($duration) { $duration = trim((string) $duration); if ($duration === '') { return 0; } if (strpos($duration, ':') !== false) { $parts = array_map('intval', explode(':', $duration)); $seconds = 0; foreach ($parts as $part) { $seconds = $seconds * 60 + $part; } return $seconds; } return max(0, (int) $duration); } /** * Déduit le type MIME d'un fichier audio depuis son extension * * @param string $url URL du fichier audio * @return string|null Type MIME (audio/mpeg, audio/mp4, audio/ogg) ou null */ function guessAudioEncodingFormat($url) { $extension = strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?? '', PATHINFO_EXTENSION)); $map = [ 'mp3' => 'audio/mpeg', 'm4a' => 'audio/mp4', 'mp4' => 'audio/mp4', 'ogg' => 'audio/ogg', 'opus' => 'audio/ogg', 'wav' => 'audio/wav' ]; return $map[$extension] ?? null; } /** * Génère le JSON-LD pour les fils d'Ariane (BreadcrumbList) * * @param array $breadcrumbs Tableau des fils d'Ariane [['name' => 'Nom', 'url' => 'URL']] * @return string JSON-LD pour les fils d'Ariane */ function generateBreadcrumbJsonLd($breadcrumbs) { $baseUrl = getBaseUrl(); $listItems = []; foreach ($breadcrumbs as $index => $crumb) { $listItems[] = [ "@type" => "ListItem", "position" => $index + 1, "name" => $crumb['name'], "item" => $crumb['url'] ]; } $data = [ "@context" => "https://schema.org", "@type" => "BreadcrumbList", "itemListElement" => $listItems ]; return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } /** * Génère le JSON-LD pour une page de collection de vidéos * * @param string $name Nom de la collection * @param string $description Description de la collection * @param array $videos Tableau des vidéos * @param string $url URL de la page de collection * @return string JSON-LD pour la collection */ function generateVideoCollectionJsonLd($name, $description, $videos, $url) { $baseUrl = getBaseUrl(); $videoItems = []; foreach ($videos as $video) { $videoItems[] = [ "@type" => "VideoObject", "name" => $video['title'], "url" => $baseUrl . "/video.php?id=" . $video['id'], "thumbnailUrl" => $video['thumbnail'], "uploadDate" => formatDateISO8601($video['date']), "duration" => formatDurationISO8601($video['duration'] ?? 0) ]; } $data = [ "@context" => "https://schema.org", "@type" => "CollectionPage", "name" => $name, "description" => $description, "url" => $url, "mainEntity" => [ "@type" => "ItemList", "itemListElement" => $videoItems, "numberOfItems" => count($videos) ], "publisher" => [ "@type" => "Organization", "name" => SITE_NAME, "url" => $baseUrl ] ]; return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } /** * Formate une durée en secondes au format ISO 8601 (PTnHnMnS) * * @param int $seconds Durée en secondes * @return string Durée au format ISO 8601 */ function formatDurationISO8601($seconds) { $hours = floor($seconds / 3600); $minutes = floor(($seconds % 3600) / 60); $remainingSeconds = $seconds % 60; $duration = 'PT'; if ($hours > 0) { $duration .= $hours . 'H'; } if ($minutes > 0) { $duration .= $minutes . 'M'; } if ($remainingSeconds > 0 || ($hours === 0 && $minutes === 0)) { $duration .= $remainingSeconds . 'S'; } return $duration; } /** * Formate une date au format ISO 8601 * * @param string $dateString Date à formater * @return string Date au format ISO 8601 */ function formatDateISO8601($dateString) { try { $date = new DateTime($dateString); return $date->format('c'); // Format ISO 8601 } catch (Exception $e) { // En cas d'erreur, retourner la date actuelle return (new DateTime())->format('c'); } } /** * Tronque un texte à une longueur donnée * * @param string $text Texte à tronquer * @param int $length Longueur maximale * @return string Texte tronqué */ function truncateText($text, $length = 200) { if (strlen($text) <= $length) { return $text; } $truncated = substr($text, 0, $length); $lastSpace = strrpos($truncated, ' '); if ($lastSpace !== false) { $truncated = substr($truncated, 0, $lastSpace); } return $truncated . '...'; } /** * Obtient l'URL de base du site * * @return string URL de base */ function getBaseUrl() { $scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; return $scheme . '://' . $host; } /** * Génère et affiche un script JSON-LD * * @param string $jsonLd Données JSON-LD */ function outputJsonLd($jsonLd) { $nonce = getCspNonce(); echo '' . "\n"; } ?>