fix & feat : SEO desc, feed cover, livres slug auto + filtre (v1.6.21)

- 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 : <media:thumbnail> 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 11:05:01 +02:00
parent 3b22be94e8
commit ca6cfa4ebf
8 changed files with 92 additions and 27 deletions
+32
View File
@@ -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;
});
}
});
+8 -6
View File
@@ -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;
+8
View File
@@ -80,6 +80,7 @@ echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:media="http://search.yahoo.com/mrss/"
xmlns:fh="http://purl.org/syndication/history/1.0">
<channel>
<title><?= htmlspecialchars($channelTitle) ?></title>
@@ -111,6 +112,10 @@ echo '<?xml version="1.0" encoding="UTF-8"?>' . "\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']);
}
?>
<item>
<title><?= $title ?></title>
@@ -119,6 +124,9 @@ echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
<?php if ($fullHtml !== ''): ?>
<content:encoded><![CDATA[<?= $fullHtml ?>]]></content:encoded>
<?php endif; ?>
<?php if ($imgUrl !== ''): ?>
<media:thumbnail url="<?= htmlspecialchars($imgUrl, ENT_XML1) ?>"/>
<?php endif; ?>
<pubDate><?= htmlspecialchars($pubDate) ?></pubDate>
<guid isPermaLink="true"><?= $guid ?></guid>
</item>
+19 -12
View File
@@ -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]);
}
+1 -1
View File
@@ -1 +1 @@
1.6.20
1.6.21