v1.6.25 — intégration IA éditeur, onglet admin IA, corrections CSP #98
@@ -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 `<media:thumbnail>` 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é
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -1 +1 @@
|
||||
1.6.20
|
||||
1.6.21
|
||||
|
||||
+1
-1
@@ -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',
|
||||
|
||||
+10
-7
@@ -1342,6 +1342,8 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-medium">Ajouter une page existante</label>
|
||||
<input type="text" id="book-article-filter" class="form-control form-control-sm mb-1"
|
||||
placeholder="Filtrer les articles…" autocomplete="off">
|
||||
<select class="form-select" id="book-article-select">
|
||||
<option value="">— Choisir un article —</option>
|
||||
<?php
|
||||
@@ -1377,15 +1379,16 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
|
||||
<?php elseif (isset($_GET['new'])): ?>
|
||||
<h5>Nouveau livre</h5>
|
||||
<form method="POST" action="/?action=book_save">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-medium">Slug (identifiant URL)</label>
|
||||
<input type="text" name="slug" class="form-control" required
|
||||
placeholder="mon-livre" pattern="[a-z0-9][a-z0-9-]*">
|
||||
<div class="form-text">Minuscules, chiffres, tirets. Exemple : <code>esp8266</code></div>
|
||||
</div>
|
||||
<input type="hidden" name="slug" id="new-book-slug-hidden">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-medium">Titre</label>
|
||||
<input type="text" name="title" class="form-control" required placeholder="Titre du livre">
|
||||
<input type="text" name="title" id="new-book-title" class="form-control" required placeholder="Titre du livre">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small fw-medium">Slug (généré automatiquement)</label>
|
||||
<input type="text" id="new-book-slug-preview" class="form-control form-control-sm bg-light font-monospace"
|
||||
readonly placeholder="slug-du-livre">
|
||||
<div class="form-text">Minuscules, chiffres, tirets — basé sur le titre.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small fw-medium">Description (optionnelle)</label>
|
||||
|
||||
Reference in New Issue
Block a user