feat : RSS content, feed catégorie, cookie commentaires, flux erreurs, email preview (v1.6.17)
- RSS : content:encoded (HTML complet) + fix description via plain (#42) - RSS : flux filtré par ?category=nom (#43) - Commentaires : cookie nom/email pour pré-remplir le formulaire (#51) - flux/ : bandeau admin des feeds en erreur (#45) - admin/emails : bouton « Voir ↗ » vers /admin/email-preview/{id} en nouvel onglet (#37) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<content:encoded>` 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 : `<description>` utilise désormais le champ `plain` pré-calculé (fix : contenu vide depuis v1.6.14) (#42)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.6.16] - 2026-05-16
|
## [1.6.16] - 2026-05-16
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
+2
-1
@@ -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 ^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]
|
RewriteRule ^import/([0-9a-f-]{36})/?$ /index.php?action=import_image&uuid=$1 [L,QSA]
|
||||||
|
|
||||||
# Admin (regen-thumbs et role/<email> avant la règle générique admin/<tab>)
|
# Admin (regen-thumbs, email-preview et role/<email> avant la règle générique admin/<tab>)
|
||||||
RewriteRule ^admin/regen-thumbs/?$ /index.php?action=regen_thumbs [L,QSA]
|
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/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/([a-z0-9-]+)/?$ /index.php?action=admin&tab=$1 [L,QSA]
|
||||||
RewriteRule ^admin/?$ /index.php?action=admin [L,QSA]
|
RewriteRule ^admin/?$ /index.php?action=admin [L,QSA]
|
||||||
|
|||||||
+25
-8
@@ -18,15 +18,22 @@ $Parsedown = new Parsedown();
|
|||||||
|
|
||||||
$now = time();
|
$now = time();
|
||||||
$base = rtrim(APP_URL, '/');
|
$base = rtrim(APP_URL, '/');
|
||||||
|
$filterCat = trim($_GET['category'] ?? '');
|
||||||
|
|
||||||
$all = array_values(array_filter(
|
$all = array_values(array_filter(
|
||||||
$articles->getAll(publishedOnly: true),
|
$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) {
|
if (strtotime((string)($a['published_at'] ?? '')) > $now) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$cat = trim($a['category'] ?? '');
|
$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;
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -47,8 +54,11 @@ $nextCursor = (count($all) > $offset + FEED_PAGE_SIZE)
|
|||||||
? ($all[$offset + FEED_PAGE_SIZE - 1]['uuid'] ?? null)
|
? ($all[$offset + FEED_PAGE_SIZE - 1]['uuid'] ?? null)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
$feedUrl = $base . '/feed';
|
$feedUrl = $base . '/feed' . ($filterCat !== '' ? '?category=' . rawurlencode($filterCat) : '');
|
||||||
$feedNextUrl = $nextCursor !== null ? $base . '/feed/' . $nextCursor : null;
|
$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 ───────────────────────────────────────────────────────────
|
// ─── lastBuildDate ───────────────────────────────────────────────────────────
|
||||||
$lastBuild = '';
|
$lastBuild = '';
|
||||||
@@ -69,11 +79,12 @@ echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|||||||
?>
|
?>
|
||||||
<rss version="2.0"
|
<rss version="2.0"
|
||||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
||||||
|
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
||||||
xmlns:fh="http://purl.org/syndication/history/1.0">
|
xmlns:fh="http://purl.org/syndication/history/1.0">
|
||||||
<channel>
|
<channel>
|
||||||
<title><?= htmlspecialchars(siteTitle()) ?></title>
|
<title><?= htmlspecialchars($channelTitle) ?></title>
|
||||||
<link><?= htmlspecialchars($base) ?></link>
|
<link><?= htmlspecialchars($base) ?></link>
|
||||||
<description><?= htmlspecialchars(siteClaim()) ?></description>
|
<description><?= htmlspecialchars($channelDesc) ?></description>
|
||||||
<language><?= htmlspecialchars(siteLang()) ?></language>
|
<language><?= htmlspecialchars(siteLang()) ?></language>
|
||||||
<lastBuildDate><?= htmlspecialchars($lastBuild) ?></lastBuildDate>
|
<lastBuildDate><?= htmlspecialchars($lastBuild) ?></lastBuildDate>
|
||||||
|
|
||||||
@@ -94,14 +105,20 @@ echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
|
|||||||
$pubDate = date(DATE_RSS, (int)strtotime((string)($article['published_at'] ?? $article['created_at'] ?? '')));
|
$pubDate = date(DATE_RSS, (int)strtotime((string)($article['published_at'] ?? $article['created_at'] ?? '')));
|
||||||
$link = $base . '/post/' . rawurlencode($article['slug'] ?? '');
|
$link = $base . '/post/' . rawurlencode($article['slug'] ?? '');
|
||||||
$title = htmlspecialchars($article['title'] ?? '', ENT_XML1);
|
$title = htmlspecialchars($article['title'] ?? '', ENT_XML1);
|
||||||
$plain = preg_replace('/\s+/', ' ', strip_tags($Parsedown->text($article['content'] ?? '')));
|
$plain = preg_replace('/\s+/', ' ', trim($article['plain'] ?? ''));
|
||||||
$desc = htmlspecialchars(mb_strimwidth(trim((string)$plain), 0, 300, '…'), ENT_XML1);
|
$desc = htmlspecialchars(mb_strimwidth($plain, 0, 300, '…'), ENT_XML1);
|
||||||
$guid = htmlspecialchars($base . '/post/' . rawurlencode($article['slug'] ?? ''), 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) : '';
|
||||||
?>
|
?>
|
||||||
<item>
|
<item>
|
||||||
<title><?= $title ?></title>
|
<title><?= $title ?></title>
|
||||||
<link><?= htmlspecialchars($link) ?></link>
|
<link><?= htmlspecialchars($link) ?></link>
|
||||||
<description><?= $desc ?></description>
|
<description><?= $desc ?></description>
|
||||||
|
<?php if ($fullHtml !== ''): ?>
|
||||||
|
<content:encoded><![CDATA[<?= $fullHtml ?>]]></content:encoded>
|
||||||
|
<?php endif; ?>
|
||||||
<pubDate><?= htmlspecialchars($pubDate) ?></pubDate>
|
<pubDate><?= htmlspecialchars($pubDate) ?></pubDate>
|
||||||
<guid isPermaLink="true"><?= $guid ?></guid>
|
<guid isPermaLink="true"><?= $guid ?></guid>
|
||||||
</item>
|
</item>
|
||||||
|
|||||||
+31
-1
@@ -1404,6 +1404,7 @@ switch ($action) {
|
|||||||
require_once BASE_PATH . '/src/FeedFetcher.php';
|
require_once BASE_PATH . '/src/FeedFetcher.php';
|
||||||
$fetcher = new FeedFetcher(DATA_PATH . '/_cache/feeds');
|
$fetcher = new FeedFetcher(DATA_PATH . '/_cache/feeds');
|
||||||
$fluxItems = [];
|
$fluxItems = [];
|
||||||
|
$fluxErrors = [];
|
||||||
$pdo = dbPdo();
|
$pdo = dbPdo();
|
||||||
if ($pdo) {
|
if ($pdo) {
|
||||||
try {
|
try {
|
||||||
@@ -1417,6 +1418,11 @@ switch ($action) {
|
|||||||
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $_row) {
|
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $_row) {
|
||||||
$data = $fetcher->get($_row['feed_url']);
|
$data = $fetcher->get($_row['feed_url']);
|
||||||
if (!$data) {
|
if (!$data) {
|
||||||
|
$fluxErrors[] = [
|
||||||
|
'feed_url' => $_row['feed_url'],
|
||||||
|
'label' => $_row['label'],
|
||||||
|
'user_email' => $_row['user_email'],
|
||||||
|
];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$feedTitle = $_row['label'] !== '' ? $_row['label'] : $data['feed_title'];
|
$feedTitle = $_row['label'] !== '' ? $_row['label'] : $data['feed_title'];
|
||||||
@@ -2541,7 +2547,7 @@ switch ($action) {
|
|||||||
'queued' => (int)($row['queued'] ?? 0),
|
'queued' => (int)($row['queued'] ?? 0),
|
||||||
];
|
];
|
||||||
$adminData['emails'] = $pdo->query(
|
$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
|
FROM journal_smtp $whereEml
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT $emlLimit OFFSET $emlOffset"
|
LIMIT $emlLimit OFFSET $emlOffset"
|
||||||
@@ -2757,6 +2763,30 @@ switch ($action) {
|
|||||||
header('Location: /admin/smtp');
|
header('Location: /admin/smtp');
|
||||||
exit;
|
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 '<!doctype html><html lang="fr"><head><meta charset="utf-8"><title>' . htmlspecialchars((string)$emailRow['subject']) . '</title></head><body>' . $previewHtml . '</body></html>';
|
||||||
|
exit;
|
||||||
|
|
||||||
case 'admin_toggle_featured':
|
case 'admin_toggle_featured':
|
||||||
requireAuth();
|
requireAuth();
|
||||||
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.6.16
|
1.6.17
|
||||||
|
|||||||
+5
-9
@@ -1005,17 +1005,13 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
|
|||||||
<td class="small"><?= htmlspecialchars((string)$em['subject']) ?></td>
|
<td class="small"><?= htmlspecialchars((string)$em['subject']) ?></td>
|
||||||
<td><?= $emBadge ?></td>
|
<td><?= $emBadge ?></td>
|
||||||
<td>
|
<td>
|
||||||
<details>
|
<?php if (!empty($em['content_html']) || !empty($em['content_text'])): ?>
|
||||||
<summary class="btn btn-outline-secondary btn-sm" style="display:inline;cursor:pointer">Voir</summary>
|
<a href="/admin/email-preview/<?= (int)$em['id'] ?>" target="_blank" rel="noopener"
|
||||||
<div class="mt-2 p-2 border rounded bg-light" style="max-width:600px">
|
class="btn btn-outline-secondary btn-sm">Voir ↗</a>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if (!empty($em['error_message'])): ?>
|
<?php if (!empty($em['error_message'])): ?>
|
||||||
<p class="text-danger small mb-2"><strong>Erreur :</strong> <?= htmlspecialchars((string)$em['error_message']) ?></p>
|
<span class="text-danger small d-block mt-1" title="<?= htmlspecialchars((string)$em['error_message']) ?>">⚠ Erreur</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (!empty($em['content_text'])): ?>
|
|
||||||
<pre class="mb-0 small" style="white-space:pre-wrap;font-size:0.75rem"><?= htmlspecialchars((string)$em['content_text']) ?></pre>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
@@ -142,3 +142,29 @@ setcookie('_csrf_c', $_csrfToken, [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var maxAge = 365 * 24 * 3600;
|
||||||
|
function getCookie(name) {
|
||||||
|
var m = document.cookie.match('(?:^|; )' + name + '=([^;]*)');
|
||||||
|
return m ? decodeURIComponent(m[1]) : '';
|
||||||
|
}
|
||||||
|
function setCookie(name, value) {
|
||||||
|
document.cookie = name + '=' + encodeURIComponent(value) + ';max-age=' + maxAge + ';path=/;SameSite=Lax';
|
||||||
|
}
|
||||||
|
var nameEl = document.getElementById('comment-name');
|
||||||
|
var emailEl = document.getElementById('comment-email');
|
||||||
|
if (!nameEl || !emailEl) { return; }
|
||||||
|
var savedName = getCookie('cmt_name');
|
||||||
|
var savedEmail = getCookie('cmt_email');
|
||||||
|
if (savedName) { nameEl.value = savedName; }
|
||||||
|
if (savedEmail) { emailEl.value = savedEmail; }
|
||||||
|
var form = document.getElementById('comment-form');
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', function () {
|
||||||
|
if (nameEl.value.trim()) { setCookie('cmt_name', nameEl.value.trim()); }
|
||||||
|
if (emailEl.value.trim()) { setCookie('cmt_email', emailEl.value.trim()); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}());
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
<h1 class="h4 mb-0">Flux agrégés</h1>
|
<h1 class="h4 mb-0">Flux agrégés</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($fluxErrors) && function_exists('isAdmin') && isAdmin()): ?>
|
||||||
|
<div class="alert alert-warning py-2 mb-4">
|
||||||
|
<strong><?= count($fluxErrors) ?> flux en erreur</strong>
|
||||||
|
<ul class="mb-0 mt-1 small">
|
||||||
|
<?php foreach ($fluxErrors as $_err): ?>
|
||||||
|
<li>
|
||||||
|
<?= htmlspecialchars($_err['label'] !== '' ? $_err['label'] : $_err['feed_url']) ?>
|
||||||
|
— <code><?= htmlspecialchars($_err['feed_url']) ?></code>
|
||||||
|
<span class="text-muted">(<?= htmlspecialchars($_err['user_email']) ?>)</span>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if (empty($fluxItems)): ?>
|
<?php if (empty($fluxItems)): ?>
|
||||||
<p class="text-muted">Aucun article disponible pour l'instant.</p>
|
<p class="text-muted">Aucun article disponible pour l'instant.</p>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user