diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ba9bf2..9cd03bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag --- +## [1.6.17] - 2026-05-16 + +### Ajouté +- RSS : élément `` avec HTML complet par article + namespace `content` (#42) +- RSS : filtre `?category=nom` — flux filtré par catégorie, titre et description du channel adaptés (#43) +- Commentaires : cookie `cmt_name` / `cmt_email` (1 an) pour pré-remplir le formulaire à la prochaine visite (#51) +- `flux/` : bandeau d'alerte admin listant les feeds en erreur (URL, label, email) (#45) +- `admin/emails` : bouton « Voir ↗ » ouvre le contenu HTML de l'email dans un nouvel onglet via `/admin/email-preview/{id}` (#37) + +### Modifié +- RSS : `` utilise désormais le champ `plain` pré-calculé (fix : contenu vide depuis v1.6.14) (#42) + +--- + ## [1.6.16] - 2026-05-16 ### Ajouté diff --git a/public/.htaccess b/public/.htaccess index aa662e6..e33d5bc 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -41,8 +41,9 @@ RewriteRule ^diff/([0-9a-f-]{36})/(\d+)/?$ /index.php?action=diff&uuid=$1&rev=$2 RewriteRule ^files/([0-9a-f-]{36})/add/?$ /index.php?action=add_files&uuid=$1 [L,QSA] RewriteRule ^import/([0-9a-f-]{36})/?$ /index.php?action=import_image&uuid=$1 [L,QSA] -# Admin (regen-thumbs et role/ avant la règle générique admin/) +# Admin (regen-thumbs, email-preview et role/ avant la règle générique admin/) RewriteRule ^admin/regen-thumbs/?$ /index.php?action=regen_thumbs [L,QSA] +RewriteRule ^admin/email-preview/(\d+)/?$ /index.php?action=admin_email_preview&id=$1 [L,QSA] RewriteRule ^admin/role/([a-z0-9_-]+)/?$ /index.php?action=admin_role_edit&role_name=$1 [L,QSA] RewriteRule ^admin/([a-z0-9-]+)/?$ /index.php?action=admin&tab=$1 [L,QSA] RewriteRule ^admin/?$ /index.php?action=admin [L,QSA] diff --git a/public/feed.php b/public/feed.php index 2b12f95..5cdc849 100644 --- a/public/feed.php +++ b/public/feed.php @@ -16,17 +16,24 @@ $articles = new ArticleManager(DATA_PATH); $privateCats = $articles->getPrivateCategories(); $Parsedown = new Parsedown(); -$now = time(); -$base = rtrim(APP_URL, '/'); +$now = time(); +$base = rtrim(APP_URL, '/'); +$filterCat = trim($_GET['category'] ?? ''); $all = array_values(array_filter( $articles->getAll(publishedOnly: true), - static function (array $a) use ($now, $privateCats): bool { + static function (array $a) use ($now, $privateCats, $filterCat): bool { if (strtotime((string)($a['published_at'] ?? '')) > $now) { return false; } $cat = trim($a['category'] ?? ''); - return $cat === '' || !in_array($cat, $privateCats, true); + if ($cat !== '' && in_array($cat, $privateCats, true)) { + return false; + } + if ($filterCat !== '' && $cat !== $filterCat) { + return false; + } + return true; } )); @@ -42,13 +49,16 @@ if ($after !== '') { } } -$items = array_slice($all, $offset, FEED_PAGE_SIZE); +$items = array_slice($all, $offset, FEED_PAGE_SIZE); $nextCursor = (count($all) > $offset + FEED_PAGE_SIZE) ? ($all[$offset + FEED_PAGE_SIZE - 1]['uuid'] ?? null) : null; -$feedUrl = $base . '/feed'; -$feedNextUrl = $nextCursor !== null ? $base . '/feed/' . $nextCursor : null; +$feedUrl = $base . '/feed' . ($filterCat !== '' ? '?category=' . rawurlencode($filterCat) : ''); +$feedNextUrl = $nextCursor !== null ? $base . '/feed/' . $nextCursor . ($filterCat !== '' ? '?category=' . rawurlencode($filterCat) : '') : null; + +$channelTitle = siteTitle() . ($filterCat !== '' ? ' — ' . $filterCat : ''); +$channelDesc = $filterCat !== '' ? 'Articles de la catégorie « ' . $filterCat . ' »' : siteClaim(); // ─── lastBuildDate ─────────────────────────────────────────────────────────── $lastBuild = ''; @@ -69,11 +79,12 @@ echo '' . "\n"; ?> - <?= htmlspecialchars(siteTitle()) ?> + <?= htmlspecialchars($channelTitle) ?> - + @@ -91,17 +102,23 @@ echo '' . "\n"; text($article['content'] ?? ''))); - $desc = htmlspecialchars(mb_strimwidth(trim((string)$plain), 0, 300, '…'), ENT_XML1); - $guid = htmlspecialchars($base . '/post/' . rawurlencode($article['slug'] ?? ''), ENT_XML1); + $pubDate = date(DATE_RSS, (int)strtotime((string)($article['published_at'] ?? $article['created_at'] ?? ''))); + $link = $base . '/post/' . rawurlencode($article['slug'] ?? ''); + $title = htmlspecialchars($article['title'] ?? '', ENT_XML1); + $plain = preg_replace('/\s+/', ' ', trim($article['plain'] ?? '')); + $desc = htmlspecialchars(mb_strimwidth($plain, 0, 300, '…'), ENT_XML1); + $guid = htmlspecialchars($base . '/post/' . rawurlencode($article['slug'] ?? ''), ENT_XML1); + $mdPath = DATA_PATH . '/' . ($article['uuid'] ?? '') . '/index.md'; + $rawMd = file_exists($mdPath) ? (string)file_get_contents($mdPath) : ''; + $fullHtml = $rawMd !== '' ? $Parsedown->text($rawMd) : ''; ?> <?= $title ?> + + ]]> + diff --git a/public/index.php b/public/index.php index 0cc6c77..0f0ac0f 100644 --- a/public/index.php +++ b/public/index.php @@ -1402,9 +1402,10 @@ switch ($action) { case 'flux': require_once BASE_PATH . '/src/FeedFetcher.php'; - $fetcher = new FeedFetcher(DATA_PATH . '/_cache/feeds'); - $fluxItems = []; - $pdo = dbPdo(); + $fetcher = new FeedFetcher(DATA_PATH . '/_cache/feeds'); + $fluxItems = []; + $fluxErrors = []; + $pdo = dbPdo(); if ($pdo) { try { $st = $pdo->query( @@ -1417,6 +1418,11 @@ switch ($action) { foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $_row) { $data = $fetcher->get($_row['feed_url']); if (!$data) { + $fluxErrors[] = [ + 'feed_url' => $_row['feed_url'], + 'label' => $_row['label'], + 'user_email' => $_row['user_email'], + ]; continue; } $feedTitle = $_row['label'] !== '' ? $_row['label'] : $data['feed_title']; @@ -2541,7 +2547,7 @@ switch ($action) { 'queued' => (int)($row['queued'] ?? 0), ]; $adminData['emails'] = $pdo->query( - "SELECT id, created_at, to_email, subject, status, error_message, content_text, sent_at + "SELECT id, created_at, to_email, subject, status, error_message, content_text, content_html, sent_at FROM journal_smtp $whereEml ORDER BY created_at DESC LIMIT $emlLimit OFFSET $emlOffset" @@ -2757,6 +2763,30 @@ switch ($action) { header('Location: /admin/smtp'); exit; + case 'admin_email_preview': + requireAuth(); + if (!isAdmin()) { + http_response_code(403); + exit; + } + $previewId = (int)($_GET['id'] ?? 0); + $pdo = dbPdo(); + $emailRow = null; + if ($pdo && $previewId > 0) { + $st = $pdo->prepare('SELECT subject, content_html, content_text FROM journal_smtp WHERE id = :id'); + $st->execute([':id' => $previewId]); + $emailRow = $st->fetch(PDO::FETCH_ASSOC) ?: null; + } + if (!$emailRow) { + http_response_code(404); + echo 'Email introuvable.'; + exit; + } + header('Content-Type: text/html; charset=UTF-8'); + $previewHtml = !empty($emailRow['content_html']) ? $emailRow['content_html'] : nl2br(htmlspecialchars((string)$emailRow['content_text'])); + echo '' . htmlspecialchars((string)$emailRow['subject']) . '' . $previewHtml . ''; + exit; + case 'admin_toggle_featured': requireAuth(); if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') { diff --git a/public/version.txt b/public/version.txt index 9494224..5f3f715 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -1.6.16 +1.6.17 diff --git a/templates/admin.php b/templates/admin.php index 6acda9b..ea3bd29 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -1005,17 +1005,13 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb): -
- Voir -
- -

Erreur :

- - -
- -
-
+ + Voir ↗ + + + ⚠ Erreur + diff --git a/templates/comments_section.php b/templates/comments_section.php index f6af609..6fee5b8 100644 --- a/templates/comments_section.php +++ b/templates/comments_section.php @@ -142,3 +142,29 @@ setcookie('_csrf_c', $_csrfToken, [ + diff --git a/templates/flux.php b/templates/flux.php index 401e912..9d98e97 100644 --- a/templates/flux.php +++ b/templates/flux.php @@ -4,6 +4,21 @@

Flux agrégés

+ +
+ flux en erreur +
    + +
  • + + — + () +
  • + +
+
+ +

Aucun article disponible pour l'instant.