From ca6cfa4ebfb655d7eebeaf40bb552c6863d296d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9drix?= Date: Sat, 16 May 2026 11:05:01 +0200 Subject: [PATCH] fix & feat : SEO desc, feed cover, livres slug auto + filtre (v1.6.21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buildAutoSeoDesc() : entités HTML décodées + titre supprimé en tête (#91) - post_confirm.js : guard null sur #confirm-slug absent (#91) - feed.php : avec image de couverture RSS (#90) - admin livres : slug auto depuis le titre + filtre articles (#89) - BookManager::sanitizeSlug() passé public Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 13 +++++++++++++ public/assets/js/admin.js | 32 ++++++++++++++++++++++++++++++++ public/assets/js/post_confirm.js | 14 ++++++++------ public/feed.php | 8 ++++++++ public/index.php | 31 +++++++++++++++++++------------ public/version.txt | 2 +- src/BookManager.php | 2 +- templates/admin.php | 17 ++++++++++------- 8 files changed, 92 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69cf609..9b46804 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/) — versionnag --- +## [1.6.21] - 2026-05-16 + +### Ajouté +- Feed RSS : balise `` avec l'image de couverture de l'article (namespace `media:`) (#90) +- Admin livres : slug généré automatiquement depuis le titre à la création (#89) +- Admin livres : champ de filtre texte en temps réel sur le sélecteur « Ajouter une page » (#89) + +### Corrigé +- `autoSeoDesc` : décodage des entités HTML (`&`, ` `…) + suppression du titre en tête de description (#91) +- `post_confirm.js` : guard null sur `#confirm-slug` absent (étape 5 du wizard) — plus d'erreur JS (#91) + +--- + ## [1.6.20] - 2026-05-16 ### Ajouté diff --git a/public/assets/js/admin.js b/public/assets/js/admin.js index 227d996..5770675 100644 --- a/public/assets/js/admin.js +++ b/public/assets/js/admin.js @@ -85,5 +85,37 @@ document.addEventListener('DOMContentLoaded', function () { if (lines.indexOf(slug) === -1) { lines.push(slug); ta.value = lines.join('\n'); } bookArticleSel.value = ''; }); + + // Filtre texte en temps réel pour le sélecteur d'articles + var bookFilter = document.getElementById('book-article-filter'); + if (bookFilter) { + var bookOptions = Array.from(bookArticleSel.options); + bookFilter.addEventListener('input', function () { + var q = bookFilter.value.trim().toLowerCase(); + bookArticleSel.innerHTML = ''; + bookOptions.forEach(function (opt) { + if (opt.value === '' || q === '' || opt.textContent.toLowerCase().includes(q)) { + bookArticleSel.appendChild(opt.cloneNode(true)); + } + }); + }); + } + } + + // Slug auto pour la création d'un livre + var newBookTitle = document.getElementById('new-book-title'); + var newBookSlugPreview = document.getElementById('new-book-slug-preview'); + var newBookSlugHidden = document.getElementById('new-book-slug-hidden'); + if (newBookTitle && newBookSlugPreview && newBookSlugHidden) { + function toBookSlug(s) { + var map = { 'à':'a','â':'a','ä':'a','é':'e','è':'e','ê':'e','ë':'e','î':'i','ï':'i','ô':'o','ö':'o','ù':'u','û':'u','ü':'u','ç':'c','æ':'ae','œ':'oe' }; + s = s.toLowerCase().replace(/[àâäéèêëîïôöùûüçæœ]/g, function (c) { return map[c] || c; }); + return s.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''); + } + newBookTitle.addEventListener('input', function () { + var slug = toBookSlug(newBookTitle.value); + newBookSlugPreview.value = slug; + newBookSlugHidden.value = slug; + }); } }); diff --git a/public/assets/js/post_confirm.js b/public/assets/js/post_confirm.js index 2c0834f..051b8c6 100644 --- a/public/assets/js/post_confirm.js +++ b/public/assets/js/post_confirm.js @@ -22,12 +22,14 @@ document.addEventListener('DOMContentLoaded', function () { initCounter('seo_description', 'seo_desc_counter', 155); function updatePreview() { - var seoTitle = document.getElementById('seo_title').value.trim(); - var seoDesc = document.getElementById('seo_description').value.trim(); - var slug = document.getElementById('confirm-slug').value.trim(); + var seoTitle = document.getElementById('seo_title').value.trim(); + var seoDesc = document.getElementById('seo_description').value.trim(); + var slugEl = document.getElementById('confirm-slug'); document.getElementById('preview-title').textContent = seoTitle || defaultTitle; document.getElementById('preview-desc').textContent = seoDesc || defaultDesc; - document.getElementById('preview-url').textContent = baseUrl + slug; + if (slugEl) { + document.getElementById('preview-url').textContent = baseUrl + slugEl.value.trim(); + } } ['seo_title', 'seo_description', 'confirm-slug'].forEach(function (id) { @@ -39,7 +41,7 @@ document.addEventListener('DOMContentLoaded', function () { var slugDisplay = document.getElementById('slug-display'); var btnSuggest = document.getElementById('slug-btn-suggest'); - if (btnSuggest) { + if (btnSuggest && slugInput && slugDisplay) { btnSuggest.addEventListener('click', function () { var val = btnSuggest.dataset.slugSuggest; slugInput.value = val; @@ -49,7 +51,7 @@ document.addEventListener('DOMContentLoaded', function () { } var btnKeep = document.getElementById('slug-btn-keep'); - if (btnKeep) { + if (btnKeep && slugInput && slugDisplay) { btnKeep.addEventListener('click', function () { var val = btnKeep.dataset.slugKeep; slugInput.value = val; diff --git a/public/feed.php b/public/feed.php index 5cdc849..15a5b9e 100644 --- a/public/feed.php +++ b/public/feed.php @@ -80,6 +80,7 @@ echo '' . "\n"; <?= htmlspecialchars($channelTitle) ?> @@ -111,6 +112,10 @@ echo '' . "\n"; $mdPath = DATA_PATH . '/' . ($article['uuid'] ?? '') . '/index.md'; $rawMd = file_exists($mdPath) ? (string)file_get_contents($mdPath) : ''; $fullHtml = $rawMd !== '' ? $Parsedown->text($rawMd) : ''; + $imgUrl = trim($article['og_image'] ?? ''); + if ($imgUrl === '' && ($article['cover'] ?? '') !== '') { + $imgUrl = $base . '/file?uuid=' . rawurlencode($article['uuid']) . '&name=' . rawurlencode($article['cover']); + } ?> <?= $title ?> @@ -119,6 +124,9 @@ echo '' . "\n"; ]]> + + + diff --git a/public/index.php b/public/index.php index 87cabff..6618c23 100644 --- a/public/index.php +++ b/public/index.php @@ -69,6 +69,19 @@ function log404(string $url): void @file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX); } +function buildAutoSeoDesc(string $content, string $title = ''): string +{ + require_once BASE_PATH . '/src/Parsedown.php'; + $_pd = new Parsedown(); + $_plain = trim((string)preg_replace('/\s+/', ' ', + html_entity_decode(strip_tags($_pd->text($content)), ENT_QUOTES | ENT_HTML5, 'UTF-8') + )); + if ($title !== '' && stripos($_plain, $title) === 0) { + $_plain = ltrim(substr($_plain, strlen($title))); + } + return mb_strimwidth($_plain, 0, 155, '…'); +} + function slugToSearchQuery(string $rawPath): string { return trim((string)preg_replace('/\s{2,}/', ' ', (string)preg_replace( @@ -653,10 +666,7 @@ switch ($action) { header('Location: /new'); exit; } - require_once BASE_PATH . '/src/Parsedown.php'; - $_pd = new Parsedown(); - $autoSeoDesc = mb_strimwidth(trim((string)preg_replace('/\s+/', ' ', strip_tags($_pd->text((string)($draft['content'] ?? ''))))), 0, 155, '…'); - unset($_pd); + $autoSeoDesc = buildAutoSeoDesc((string)($draft['content'] ?? ''), trim($draft['title'] ?? '')); if ($_SERVER['REQUEST_METHOD'] === 'POST') { $seoTitle = trim($_POST['seo_title'] ?? ''); $seoDesc = trim($_POST['seo_description'] ?? '') ?: $autoSeoDesc; @@ -1021,10 +1031,7 @@ switch ($action) { header('Location: /edit/' . rawurlencode($uuid) . '/6'); exit; } - require_once BASE_PATH . '/src/Parsedown.php'; - $_pd = new Parsedown(); - $autoSeoDesc = mb_strimwidth(trim((string)preg_replace('/\s+/', ' ', strip_tags($_pd->text((string)($draft['content'] ?? ''))))), 0, 155, '…'); - unset($_pd); + $autoSeoDesc = buildAutoSeoDesc((string)($draft['content'] ?? ''), trim($draft['title'] ?? '')); $title = $draft['title']; $seoTitle = $draft['seo_title'] ?? ''; $seoDescription = $draft['seo_description'] ?? ''; @@ -1046,10 +1053,7 @@ switch ($action) { exit; } $draftData = $articles->getDraftOverlay($uuid) ?? $article; - require_once BASE_PATH . '/src/Parsedown.php'; - $_pd = new Parsedown(); - $autoSeoDesc = mb_strimwidth(trim((string)preg_replace('/\s+/', ' ', strip_tags($_pd->text((string)($draftData['content'] ?? ''))))), 0, 155, '…'); - unset($_pd); + $autoSeoDesc = buildAutoSeoDesc((string)($draftData['content'] ?? ''), trim($draftData['title'] ?? '')); $diffLines = lineDiff((string)($article['content'] ?? ''), (string)($draftData['content'] ?? '')); $titleChanged = ($draftData['title'] ?? '') !== ($article['title'] ?? ''); $postSlug = $draftData['slug'] ?? $article['slug']; @@ -3491,6 +3495,9 @@ switch ($action) { $bTitle = trim($_POST['title'] ?? ''); $bDesc = trim($_POST['description'] ?? ''); $bArts = array_values(array_filter(array_map('trim', preg_split('/[\r\n]+/', $_POST['articles'] ?? '')))); + if ($bSlug === '' && $bTitle !== '') { + $bSlug = $books->sanitizeSlug($bTitle); + } if ($bSlug !== '' && $bTitle !== '') { $books->save(['slug' => $bSlug, 'title' => $bTitle, 'description' => $bDesc, 'articles' => $bArts]); } diff --git a/public/version.txt b/public/version.txt index c45801e..49e1fe3 100644 --- a/public/version.txt +++ b/public/version.txt @@ -1 +1 @@ -1.6.20 +1.6.21 diff --git a/src/BookManager.php b/src/BookManager.php index 6d6749c..c4bc060 100644 --- a/src/BookManager.php +++ b/src/BookManager.php @@ -117,7 +117,7 @@ class BookManager return $this->booksDir . '/' . $slug . '.json'; } - private function sanitizeSlug(string $slug): string + public function sanitizeSlug(string $slug): string { $map = [ 'à' => 'a', 'â' => 'a', 'ä' => 'a', diff --git a/templates/admin.php b/templates/admin.php index a260c25..6f6ff58 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -1342,6 +1342,8 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
+ -
Minuscules, chiffres, tirets. Exemple : esp8266
-
+
- + +
+
+ + +
Minuscules, chiffres, tirets — basé sur le titre.