'a','â' => 'a','ä' => 'a','á' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e', 'î' => 'i','ï' => 'i','í' => 'i','ô' => 'o','ö' => 'o','ó' => 'o','ù' => 'u','û' => 'u', 'ü' => 'u','ú' => 'u','ç' => 'c','ñ' => 'n','æ' => 'ae','œ' => 'oe', ]; $_tocItems = []; $_tocSeen = []; // Cache du rendu Markdown (invalidé si index.md est plus récent) $_mdFile = defined('DATA_PATH') ? DATA_PATH . '/' . ($article['uuid'] ?? '') . '/index.md' : ''; $_cacheFile = defined('DATA_PATH') ? DATA_PATH . '/' . ($article['uuid'] ?? '') . '/_cache/content_rendered.json' : ''; $_mdMtime = ($_mdFile !== '' && file_exists($_mdFile)) ? (int)filemtime($_mdFile) : 0; $_renderedContent = null; if ($_cacheFile !== '' && file_exists($_cacheFile)) { $_tmp = json_decode((string)file_get_contents($_cacheFile), true); if (is_array($_tmp) && isset($_tmp['ts'], $_tmp['html'], $_tmp['toc']) && (int)$_tmp['ts'] >= $_mdMtime && $_mdMtime > 0) { $_renderedContent = $_tmp['html']; $_tocItems = $_tmp['toc']; } } if ($_renderedContent === null) { // Le titre H1 est déjà affiché par le template ; on le retire du rendu. $_rawForRender = preg_replace('/^\s*# [^\n]*\n*/u', '', $rawContent); $_renderedContent = preg_replace_callback( '/<(h[23])>(.+?)<\/h[23]>/i', function ($m) use (&$_tocItems, &$_tocSeen, $_accentMap) { $tag = $m[1]; $inner = $m[2]; $level = (int) substr($tag, 1); $plain = strip_tags($inner); $slug = trim(preg_replace( '/[^a-z0-9]+/', '-', mb_strtolower(strtr($plain, $_accentMap), 'UTF-8') ), '-') ?: 'section'; if (isset($_tocSeen[$slug])) { $_tocSeen[$slug]++; $id = $slug . '-' . $_tocSeen[$slug]; } else { $_tocSeen[$slug] = 0; $id = $slug; } $_tocItems[] = ['level' => $level, 'text' => $plain, 'id' => $id]; return "<{$tag} id=\"" . htmlspecialchars($id) . "\">{$inner}"; }, $Parsedown->text($_rawForRender) ); $_renderedContent = typographieHtml($_renderedContent ?? ''); // Lazy loading sur toutes les images du contenu $_renderedContent = preg_replace('/]*)>/i', '', $_renderedContent ?? '') ?? $_renderedContent; // Écriture du cache if ($_cacheFile !== '' && $_mdMtime > 0) { @mkdir(dirname($_cacheFile), 0755, true); @file_put_contents($_cacheFile, json_encode( ['ts' => $_mdMtime, 'html' => $_renderedContent, 'toc' => $_tocItems], JSON_UNESCAPED_UNICODE )); } } ob_start(); $coverFile = $article['cover'] ?? ''; $ogImage = $coverFile !== '' ? url('file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($coverFile)) : null; $category = trim((string)($article['category'] ?? '')); $gradient = coverGradient($category !== '' ? $category : $article['uuid'], $allCats ?? []); // Pièces jointes (hors fichiers intégrés, thumbs et cover) $attachments = []; if ($files) { $referenced = []; preg_match_all('/\(\/file\?uuid=[^&]+&name=([^)]+)\)/', $rawContent, $m); foreach ($m[1] as $encodedName) { $referenced[rawurldecode($encodedName)] = true; } $attachments = array_values(array_filter( $files, static fn ($f) => !isset($referenced[$f['name']]) && !str_starts_with($f['name'], '_thumb_') && $f['name'] !== $coverFile )); } $externalLinks = $article['external_links'] ?? []; ?>
📖 Chapitre / — Voir le sommaire →
Brouillon
Privé
0 && $_publishedTs > 0 && $_updatedTs > $_publishedTs) { $_frMonths = ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre']; $modDate = 'Modifié le ' . (int)date('j', $_updatedTs) . ' ' . $_frMonths[(int)date('n', $_updatedTs) - 1] . ' ' . date('Y', $_updatedTs) . ' à ' . date('H', $_updatedTs) . 'h' . date('i', $_updatedTs); } $hasCover = $coverFile !== ''; $heroExtraClass = $hasCover ? '' : ' article-cover--gradient'; $heroStyle = $hasCover ? '' : ' style="background:' . htmlspecialchars($gradient) . '"'; $hasSources = (!empty($externalLinks) || !empty($files)) && function_exists('canDoOnArticle') && canDoOnArticle('view_sources', $article); ?>
> <?= htmlspecialchars($article['title']) ?>
← Retour ✎ Modifier 🗑 Supprimer

·

ℹ Sources ['👍', 'Utile'], 'important' => ['🔥', 'Important'], 'interesting' => ['🤔', 'À creuser'], ]; ?>
[$icon, $label]): ?>
Suppression impossible — droits insuffisants sur le répertoire de données.
← Précédent Premier chapitre ☰ Suivant → Dernier chapitre
0 || isLoggedIn()): ?>
Note : /5 ( vote 1 ? 's' : '' ?>) Pas encore noté
(votre note)
Connectez-vous pour noter
Partager : ✉ Mail X in 🐘
= 3): ?>
Table des matières
  • À lire aussi
  • Réactions
  • Commentaires
Rétroliens
Pièces jointes
▶
♪
📎
Ko · ·
Liens & sources
📑
↗
· · PDF p.
text($article['content'])); $plain = preg_replace('/\s+/', ' ', $plain); $seoDescription = mb_strimwidth(trim((string)$plain), 0, 155, '…'); } // og:image : cover puis fallback og_image du meta if ($ogImage === null || $ogImage === '') { $ogImage = $article['og_image'] ?? ''; } // Auteur : nom et URL de profil résolus depuis le champ author du JSON de l'article $metaAuthor = $authorName; $metaAuthorUrl = $authorProfileUrl; // JSON-LD Article $jsonLdData = [ '@context' => 'https://schema.org', '@type' => 'BlogPosting', 'headline' => $seoTitle, 'description' => $seoDescription, 'url' => $canonical, 'datePublished' => date('c', strtotime((string)$articlePublishedAt)), 'dateModified' => date('c', strtotime((string)($article['updated_at'] ?? $articlePublishedAt))), 'author' => array_filter([ '@type' => 'Person', 'name' => $metaAuthor !== '' ? $metaAuthor : siteTitle(), 'url' => $metaAuthorUrl !== '' ? $metaAuthorUrl : null, ]), 'publisher' => [ '@type' => 'Blog', 'name' => siteTitle(), 'url' => rtrim(APP_URL, '/'), ], 'inLanguage' => siteLang(), ]; if (!empty($ogImage)) { $jsonLdData['image'] = $ogImage; } $jsonLd = json_encode($jsonLdData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); include __DIR__ . '/layout.php';