Files

355 lines
16 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once __DIR__ . '/../src/Parsedown.php';
$Parsedown = new Parsedown();
$_accentMap = [
'à' => 'a','â' => 'a','ä' => 'a','á' => 'a','é' => 'e','è' => 'e','ê' => 'e','ë' => 'e',
'î' => 'i','ï' => 'i','í' => 'i','ô' => 'o','ö' => 'o','ó' => 'o','ù' => 'u','û' => 'u',
'ü' => 'u','ú' => 'u','ç' => 'c','ñ' => 'n','æ' => 'ae','œ' => 'oe',
];
$_tocItems = [];
$_tocSeen = [];
$_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}</{$tag}>";
},
$Parsedown->text($rawContent)
);
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'] ?? [];
?>
<div class="row g-4 align-items-start flex-lg-nowrap">
<!-- Colonne principale -->
<div class="col">
<div class="card mb-4">
<?php if (!$article['published']): ?>
<div class="draft-ribbon">Brouillon</div>
<?php elseif ($isPrivateCat ?? false): ?>
<div class="private-ribbon">Privé</div>
<?php endif; ?>
<?php
$authorEmail = $article['author'] ?? '';
$authorName = ($authorEmail !== '' && function_exists('authorDisplayName')) ? authorDisplayName($authorEmail) : '';
$authorProfileUrl = ($authorEmail !== '' && function_exists('authorProfileUrl')) ? authorProfileUrl($authorEmail) : '';
$authorSlugVal = ($authorEmail !== '' && function_exists('authorSlug')) ? authorSlug($authorEmail) : '';
$pubDate = htmlspecialchars(date('d/m/Y', strtotime((string)($article['published_at'] ?? $article['created_at'] ?? ''))));
$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);
?>
<div class="article-cover article-cover--hero<?= $heroExtraClass ?>"<?= $heroStyle ?>>
<?php if ($hasCover): ?>
<img src="/file?uuid=<?= rawurlencode($article['uuid']) ?>&name=<?= rawurlencode($coverFile) ?>"
alt="<?= htmlspecialchars($article['title']) ?>">
<?php endif; ?>
<div class="article-hero-text">
<!-- Haut : retour + actions admin -->
<div class="article-hero-top">
<a href="/" class="hero-btn">← Retour</a>
<?php if (function_exists('isAdmin') && isAdmin()): ?>
<a href="/edit/<?= rawurlencode($article['uuid']) ?>" class="hero-btn ms-auto">✎ Modifier</a>
<a href="/delete/<?= rawurlencode($article['uuid']) ?>"
class="hero-btn hero-btn--danger"
data-confirm="Supprimer cet article définitivement ?">🗑 Supprimer</a>
<?php endif; ?>
</div>
<!-- Bas : titre + actions secondaires -->
<div class="article-hero-bottom">
<div class="article-hero-left">
<?php if ($category !== ''): ?>
<span class="cover-category"><?= htmlspecialchars($category) ?></span>
<?php endif; ?>
<h1 class="article-title"><?= htmlspecialchars($article['title']) ?></h1>
<p class="article-hero-meta">
<?php if ($authorName !== ''): ?>
<?php if ($authorSlugVal !== ''): ?>
<a href="/profil/<?= rawurlencode($authorSlugVal) ?>" class="text-reset"><?= htmlspecialchars($authorName) ?></a>
<?php else: ?>
<span><?= htmlspecialchars($authorName) ?></span>
<?php endif; ?>
<span class="mx-1 opacity-50">·</span>
<?php endif; ?>
<?= $pubDate ?>
</p>
</div>
<div class="article-hero-right">
<?php if ($hasSources): ?>
<a href="/sources/<?= rawurlencode($article['uuid']) ?>" class="hero-btn"> Sources</a>
<?php endif; ?>
<?php
$_heroReactionDefs = [
'useful' => ['👍', 'Utile'],
'important' => ['🔥', 'Important'],
'interesting' => ['🤔', 'À creuser'],
];
?>
<div class="hero-reactions" id="reactions">
<?php foreach ($_heroReactionDefs as $type => [$icon, $label]): ?>
<?php $active = in_array($type, $visitorReactions ?? [], true); ?>
<form method="post" action="/react" class="reaction-form d-inline">
<input type="hidden" name="uuid" value="<?= htmlspecialchars($article['uuid']) ?>">
<input type="hidden" name="type" value="<?= htmlspecialchars($type) ?>">
<input type="hidden" name="_back" value="/post/<?= rawurlencode($article['slug'] ?? '') ?>#reactions">
<button type="submit"
class="hero-reaction-btn<?= $active ? ' hero-reaction-btn--active' : '' ?> reaction-btn"
data-type="<?= htmlspecialchars($type) ?>"
data-uuid="<?= htmlspecialchars($article['uuid']) ?>"
title="<?= htmlspecialchars($label) ?>">
<span><?= $icon ?></span>
<span class="reaction-count" data-type="<?= htmlspecialchars($type) ?>"><?= (int)($reactionStats[$type] ?? 0) ?></span>
</button>
</form>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<div class="card-text post-content">
<?= $_renderedContent ?>
</div>
</div>
</div>
<?php include __DIR__ . '/comments_section.php'; ?>
</div><!-- /col principale -->
<div class="post-sidebar-col order-3">
<aside class="related-sidebar">
<?php if (count($_tocItems) >= 3): ?>
<h6 class="related-sidebar-title">Table des matières</h6>
<ul class="toc-list">
<?php foreach ($_tocItems as $_ti): ?>
<li class="toc-h<?= (int) $_ti['level'] ?>">
<a href="#<?= htmlspecialchars($_ti['id']) ?>"><?= htmlspecialchars($_ti['text']) ?></a>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<div class="toc-nav">
<button class="toc-nav-btn" id="toc-go-top">↑ Haut</button>
<button class="toc-nav-btn" id="toc-go-bottom">↓ Bas</button>
</div>
<ul class="toc-list mt-2">
<?php if (!empty($alsoReadArticles ?? [])): ?>
<li><a href="#also-read">À lire aussi</a></li>
<?php endif; ?>
<li><a href="#reactions">Réactions</a></li>
<li><a href="#comments">Commentaires</a></li>
</ul>
<?php if (!empty($backlinks ?? [])): ?>
<h6 class="related-sidebar-title">Rétroliens</h6>
<?php foreach ($backlinks as $_bl):
$_blCover = $_bl['cover'] ?? '';
$_blCat = trim($_bl['category'] ?? '');
$_blGradient = coverGradient($_blCat !== '' ? $_blCat : $_bl['uuid'], $allCats ?? []);
$_blDate = date('d/m/Y', strtotime((string)($_bl['published_at'] ?? $_bl['created_at'] ?? '')));
?>
<a href="/post/<?= rawurlencode($_bl['slug'] ?? '') ?>" class="related-card">
<div class="related-card-thumb" style="<?= $_blCover !== ''
? 'background-image:url(/file?uuid=' . rawurlencode($_bl['uuid']) . '&name=' . rawurlencode($_blCover) . ');background-size:cover;background-position:center'
: 'background:' . htmlspecialchars($_blGradient) ?>">
</div>
<div class="related-card-body">
<div class="related-card-title"><?= htmlspecialchars($_bl['title']) ?></div>
<div class="related-card-date"><?= $_blDate ?></div>
</div>
</a>
<?php endforeach; ?>
<?php endif; ?>
<?php if (!empty($attachments)): ?>
<?php $filesMeta = $article['files_meta'] ?? []; ?>
<h6 class="related-sidebar-title">Pièces jointes</h6>
<div class="d-flex flex-column gap-2 mb-4">
<?php foreach ($attachments as $file):
$fileUrl = '/file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($file['name']);
$fmeta = $filesMeta[$file['name']] ?? [];
$fAuthor = trim($fmeta['author'] ?? '');
$fSource = trim($fmeta['source_url'] ?? '');
?>
<a href="<?= htmlspecialchars($fileUrl) ?>" target="_blank" rel="noopener" class="source-card">
<?php if ($file['is_image']): ?>
<div class="source-card-thumb" style="background-image:url(<?= htmlspecialchars($fileUrl) ?>);background-size:cover;background-position:center"></div>
<?php elseif ($file['is_video']): ?>
<div class="source-card-thumb source-card-thumb--link">▶</div>
<?php elseif ($file['is_audio']): ?>
<div class="source-card-thumb source-card-thumb--link">♪</div>
<?php else: ?>
<div class="source-card-thumb source-card-thumb--pdf">📎</div>
<?php endif; ?>
<div class="source-card-body">
<div class="source-card-title"><?= htmlspecialchars($file['name']) ?></div>
<div class="source-card-meta">
<?= htmlspecialchars(number_format($file['size'] / 1024, 1)) ?> Ko
<?php if ($fAuthor !== ''): ?>
· <?= htmlspecialchars($fAuthor) ?>
<?php endif; ?>
<?php if ($fSource !== ''): ?>
· <?= htmlspecialchars(parse_url($fSource, PHP_URL_HOST) ?: $fSource) ?>
<?php endif; ?>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($externalLinks)): ?>
<h6 class="related-sidebar-title">Liens &amp; sources</h6>
<div class="d-flex flex-column gap-2 mb-4">
<?php foreach ($externalLinks as $lnk):
$lMeta = $lnk['meta'] ?? [];
$lTitle = $lnk['name'] ?? '';
$lUrl = $lnk['url'] ?? '';
$lHost = parse_url($lUrl, PHP_URL_HOST) ?? $lUrl;
$lDate = $lMeta['date'] ?? '';
$lSite = $lMeta['site_name'] ?? $lHost;
$lImage = $lMeta['og_image'] ?? '';
$lMime = $lMeta['mime'] ?? 'text/html';
$lPages = $lMeta['pages'] ?? null;
$lFormat = $lMeta['page_size'] ?? '';
$isPdf = ($lMime === 'application/pdf');
?>
<a href="<?= htmlspecialchars($lUrl) ?>" target="_blank" rel="noopener" class="source-card">
<?php if ($lImage && str_starts_with($lImage, '/')): ?>
<div class="source-card-thumb" style="background-image:url(<?= htmlspecialchars($lImage) ?>);background-size:cover;background-position:center"></div>
<?php elseif ($isPdf): ?>
<div class="source-card-thumb source-card-thumb--pdf">📑</div>
<?php else: ?>
<div class="source-card-thumb source-card-thumb--link">↗</div>
<?php endif; ?>
<div class="source-card-body">
<div class="source-card-title"><?= htmlspecialchars($lTitle) ?></div>
<div class="source-card-meta">
<?= htmlspecialchars($lSite) ?>
<?php if ($lDate): ?> · <?= htmlspecialchars(substr($lDate, 0, 10)) ?><?php endif; ?>
<?php if ($isPdf && $lPages): ?> · PDF <?= $lPages ?>p.<?php endif; ?>
</div>
</div>
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
</aside>
</div>
</div><!-- /row -->
<script src="/assets/js/toc.js" defer></script>
<?php
$content = ob_get_clean();
$title = htmlspecialchars($article['title']);
$seoTitle = ($article['seo_title'] ?? '') ?: $article['title'];
$ogType = 'article';
$ogUrl = url('post/' . rawurlencode($article['slug'] ?? ''));
$canonical = $ogUrl;
$articlePublishedAt = $article['published_at'] ?? '';
$mainClass = 'container-fluid';
// Auto-description depuis le contenu si le champ SEO est vide
$seoDescription = $article['seo_description'] ?? '';
if ($seoDescription === '') {
$plain = strip_tags($Parsedown->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';