perf & ux : cache getAll, fingerprint assets, Last-Modified, 404 log, row-click bulk (v1.6.19)
- getAll() : cache fichier articles_list.json, invalidé à chaque écriture (#16) - layout.php : fingerprinting ?v=<hash> sur CSS/JS pour invalidation navigateur (#18) - case 'view' : Last-Modified + 304 Not Modified pour les articles publiés (#18) - case 'not_found' : logging JSON des 404 dans _logs/not_found.jsonl (#52) - case 'view' : echo nu → templates/404.php pour brouillons/privés (#52) - admin.js : clic sur ligne tableau → toggle bulk-check (#86) 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.19] - 2026-05-16
|
||||||
|
|
||||||
|
### Ajouté
|
||||||
|
- `admin/articles` : clic sur la ligne entière pour cocher/décocher la case de sélection bulk (#86)
|
||||||
|
- Cache HTTP `Last-Modified` + réponse `304 Not Modified` pour les articles publiés (#18)
|
||||||
|
- Fingerprinting des assets CSS/JS dans `layout.php` (`?v=<hash>`) pour invalidation automatique du cache navigateur (#18)
|
||||||
|
- Cache fichier `_cache/articles_list.json` pour `getAll()` — invalidé à chaque écriture, élimine le scan complet par requête (#16)
|
||||||
|
- Logging des 404 dans `DATA_PATH/_logs/not_found.json` (url, referer, user-agent, date) (#52)
|
||||||
|
|
||||||
|
### Corrigé
|
||||||
|
- `case 'view'` : les accès refusés (brouillon, avant-première, catégorie privée) utilisent désormais `templates/404.php` au lieu d'un `echo` nu (#52)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.6.18] - 2026-05-16
|
## [1.6.18] - 2026-05-16
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|||||||
@@ -19,6 +19,23 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clic sur la ligne entière pour cocher/décocher la case de sélection
|
||||||
|
document.querySelectorAll('table tbody tr').forEach(function (tr) {
|
||||||
|
var cb = tr.querySelector('.bulk-check');
|
||||||
|
if (!cb) { return; }
|
||||||
|
tr.style.cursor = 'pointer';
|
||||||
|
tr.addEventListener('click', function (e) {
|
||||||
|
if (e.target.closest('a, button, input, label')) { return; }
|
||||||
|
cb.checked = !cb.checked;
|
||||||
|
if (checkAll) {
|
||||||
|
var total = document.querySelectorAll('.bulk-check').length;
|
||||||
|
var checked = document.querySelectorAll('.bulk-check:checked').length;
|
||||||
|
checkAll.checked = total > 0 && checked === total;
|
||||||
|
checkAll.indeterminate = checked > 0 && checked < total;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Indicateurs de traitement formulaire SMTP (config + tester connexion)
|
// Indicateurs de traitement formulaire SMTP (config + tester connexion)
|
||||||
var smtpForm = document.getElementById('smtp-config-form');
|
var smtpForm = document.getElementById('smtp-config-form');
|
||||||
if (smtpForm) {
|
if (smtpForm) {
|
||||||
|
|||||||
+46
-3
@@ -50,6 +50,25 @@ $metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' :
|
|||||||
unset($_noindexActions);
|
unset($_noindexActions);
|
||||||
|
|
||||||
// ─── Recherche de l'article le plus proche et redirection 301 ────────────────
|
// ─── Recherche de l'article le plus proche et redirection 301 ────────────────
|
||||||
|
function log404(string $url): void
|
||||||
|
{
|
||||||
|
if (!defined('DATA_PATH')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$logDir = DATA_PATH . '/_logs';
|
||||||
|
$logFile = $logDir . '/not_found.jsonl';
|
||||||
|
if (!is_dir($logDir)) {
|
||||||
|
@mkdir($logDir, 0755, true);
|
||||||
|
}
|
||||||
|
$entry = json_encode([
|
||||||
|
'ts' => date('Y-m-d H:i:s'),
|
||||||
|
'url' => $url,
|
||||||
|
'ref' => $_SERVER['HTTP_REFERER'] ?? '',
|
||||||
|
'ua' => $_SERVER['HTTP_USER_AGENT'] ?? '',
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||||
|
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
function slugToSearchQuery(string $rawPath): string
|
function slugToSearchQuery(string $rawPath): string
|
||||||
{
|
{
|
||||||
return trim((string)preg_replace('/\s{2,}/', ' ', (string)preg_replace(
|
return trim((string)preg_replace('/\s{2,}/', ' ', (string)preg_replace(
|
||||||
@@ -679,7 +698,7 @@ switch ($action) {
|
|||||||
if (!$article['published']) {
|
if (!$article['published']) {
|
||||||
if (!canDoOnArticle('view_drafts', $article)) {
|
if (!canDoOnArticle('view_drafts', $article)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo 'Article introuvable.';
|
include BASE_PATH . '/templates/404.php';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -688,7 +707,7 @@ switch ($action) {
|
|||||||
if ($article['published'] && strtotime((string)($article['published_at'] ?? '')) > time()) {
|
if ($article['published'] && strtotime((string)($article['published_at'] ?? '')) > time()) {
|
||||||
if (!hasCapability('view_previews')) {
|
if (!hasCapability('view_previews')) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo 'Article introuvable.';
|
include BASE_PATH . '/templates/404.php';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -700,10 +719,33 @@ switch ($action) {
|
|||||||
$isPrivateCat = $articleCat !== '' && in_array($articleCat, $privateCats, true);
|
$isPrivateCat = $articleCat !== '' && in_array($articleCat, $privateCats, true);
|
||||||
if ($isPrivateCat && !isLoggedIn()) {
|
if ($isPrivateCat && !isLoggedIn()) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo 'Article introuvable.';
|
include BASE_PATH . '/templates/404.php';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache HTTP : Last-Modified + 304 pour les articles publiés
|
||||||
|
if ($article['published']) {
|
||||||
|
$_uuid = $article['uuid'] ?? '';
|
||||||
|
$_mdFile = DATA_PATH . '/' . $_uuid . '/index.md';
|
||||||
|
$_mfFile = DATA_PATH . '/' . $_uuid . '/meta.json';
|
||||||
|
$_lm = max(
|
||||||
|
is_file($_mdFile) ? (int)filemtime($_mdFile) : 0,
|
||||||
|
is_file($_mfFile) ? (int)filemtime($_mfFile) : 0
|
||||||
|
);
|
||||||
|
if ($_lm > 0) {
|
||||||
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $_lm) . ' GMT');
|
||||||
|
header('Cache-Control: public, max-age=60');
|
||||||
|
$ifModSince = isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])
|
||||||
|
? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])
|
||||||
|
: false;
|
||||||
|
if ($ifModSince !== false && $_lm <= $ifModSince) {
|
||||||
|
http_response_code(304);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($_uuid, $_mdFile, $_mfFile, $_lm, $ifModSince);
|
||||||
|
}
|
||||||
|
|
||||||
$files = $articles->getFiles($article['uuid']);
|
$files = $articles->getFiles($article['uuid']);
|
||||||
|
|
||||||
// Résout les chemins de fichiers relatifs dans le contenu
|
// Résout les chemins de fichiers relatifs dans le contenu
|
||||||
@@ -3473,6 +3515,7 @@ switch ($action) {
|
|||||||
(string)(parse_url($_SERVER['REDIRECT_URL'] ?? $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''),
|
(string)(parse_url($_SERVER['REDIRECT_URL'] ?? $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''),
|
||||||
'/'
|
'/'
|
||||||
);
|
);
|
||||||
|
log404('/' . $notFoundPath);
|
||||||
$q = slugToSearchQuery($notFoundPath);
|
$q = slugToSearchQuery($notFoundPath);
|
||||||
header('Location: /search' . ($q !== '' ? '?q=' . urlencode($q) : ''), true, 302);
|
header('Location: /search' . ($q !== '' ? '?q=' . urlencode($q) : ''), true, 302);
|
||||||
exit;
|
exit;
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.6.18
|
1.6.19
|
||||||
|
|||||||
+22
-1
@@ -30,6 +30,14 @@ class ArticleManager
|
|||||||
|
|
||||||
private function loadAll(): array
|
private function loadAll(): array
|
||||||
{
|
{
|
||||||
|
$cachePath = $this->allListCachePath();
|
||||||
|
if (file_exists($cachePath)) {
|
||||||
|
$cached = json_decode((string)file_get_contents($cachePath), true);
|
||||||
|
if (is_array($cached) && $cached !== []) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$articles = [];
|
$articles = [];
|
||||||
if (!is_dir($this->dataDir)) {
|
if (!is_dir($this->dataDir)) {
|
||||||
return $articles;
|
return $articles;
|
||||||
@@ -66,6 +74,12 @@ class ArticleManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$cacheDir = dirname($cachePath);
|
||||||
|
if (!is_dir($cacheDir)) {
|
||||||
|
@mkdir($cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
@file_put_contents($cachePath, json_encode($articles, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||||
|
|
||||||
return $articles;
|
return $articles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,6 +863,7 @@ class ArticleManager
|
|||||||
$this->allCache = null;
|
$this->allCache = null;
|
||||||
@unlink($this->articleCachePath($uuid));
|
@unlink($this->articleCachePath($uuid));
|
||||||
@unlink($this->slugIndexPath());
|
@unlink($this->slugIndexPath());
|
||||||
|
@unlink($this->allListCachePath());
|
||||||
$this->removeDir($dir);
|
$this->removeDir($dir);
|
||||||
}
|
}
|
||||||
if (is_dir($dir)) {
|
if (is_dir($dir)) {
|
||||||
@@ -879,6 +894,11 @@ class ArticleManager
|
|||||||
return $this->dataDir . '/_cache/slug_index.json';
|
return $this->dataDir . '/_cache/slug_index.json';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function allListCachePath(): string
|
||||||
|
{
|
||||||
|
return $this->dataDir . '/_cache/articles_list.json';
|
||||||
|
}
|
||||||
|
|
||||||
private function buildSlugIndex(): void
|
private function buildSlugIndex(): void
|
||||||
{
|
{
|
||||||
$cacheDir = $this->dataDir . '/_cache';
|
$cacheDir = $this->dataDir . '/_cache';
|
||||||
@@ -1343,9 +1363,10 @@ class ArticleManager
|
|||||||
$this->searchIndexCache = null;
|
$this->searchIndexCache = null;
|
||||||
$uuid = $meta['uuid'] ?? basename($dir);
|
$uuid = $meta['uuid'] ?? basename($dir);
|
||||||
|
|
||||||
// Invalider le cache article et le slug index
|
// Invalider les caches article, liste et slug index
|
||||||
@unlink($this->articleCachePath($uuid));
|
@unlink($this->articleCachePath($uuid));
|
||||||
@unlink($this->slugIndexPath());
|
@unlink($this->slugIndexPath());
|
||||||
|
@unlink($this->allListCachePath());
|
||||||
|
|
||||||
file_put_contents(
|
file_put_contents(
|
||||||
$dir . '/meta.json',
|
$dir . '/meta.json',
|
||||||
|
|||||||
+13
-4
@@ -46,11 +46,20 @@
|
|||||||
|
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/assets/css/style.css">
|
<?php
|
||||||
|
$_pub = BASE_PATH . '/public/assets/';
|
||||||
|
if (!function_exists('_av')) {
|
||||||
|
function _av(string $base, string $rel): string {
|
||||||
|
$f = $base . $rel;
|
||||||
|
return '/assets/' . $rel . (is_file($f) ? '?v=' . substr(md5_file($f), 0, 8) : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<link rel="stylesheet" href="<?= _av($_pub, 'css/style.css') ?>">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body<?php if (!empty($bodyClass ?? '')): ?> class="<?= htmlspecialchars($bodyClass) ?>"<?php endif; ?>>
|
<body<?php if (!empty($bodyClass ?? '')): ?> class="<?= htmlspecialchars($bodyClass) ?>"<?php endif; ?>>
|
||||||
<script src="/assets/js/density-fouc.js"></script>
|
<script src="<?= _av($_pub, 'js/density-fouc.js') ?>"></script>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark mb-0" role="navigation" aria-label="Navigation principale">
|
<nav class="navbar navbar-expand-lg navbar-dark mb-0" role="navigation" aria-label="Navigation principale">
|
||||||
@@ -150,9 +159,9 @@ $_layoutCurrentCat = trim($_GET['cat'] ?? '');
|
|||||||
|
|
||||||
<!-- JS -->
|
<!-- JS -->
|
||||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="/assets/js/app.js"></script>
|
<script src="<?= _av($_pub, 'js/app.js') ?>"></script>
|
||||||
<?php if (isset($reactionStats)): ?>
|
<?php if (isset($reactionStats)): ?>
|
||||||
<script src="/assets/js/reactions.js"></script>
|
<script src="<?= _av($_pub, 'js/reactions.js') ?>"></script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user