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:
2026-05-16 10:44:08 +02:00
parent 11399a54a6
commit 5ce91da06a
6 changed files with 113 additions and 9 deletions
+46 -3
View File
@@ -50,6 +50,25 @@ $metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' :
unset($_noindexActions);
// ─── 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
{
return trim((string)preg_replace('/\s{2,}/', ' ', (string)preg_replace(
@@ -679,7 +698,7 @@ switch ($action) {
if (!$article['published']) {
if (!canDoOnArticle('view_drafts', $article)) {
http_response_code(404);
echo 'Article introuvable.';
include BASE_PATH . '/templates/404.php';
exit;
}
}
@@ -688,7 +707,7 @@ switch ($action) {
if ($article['published'] && strtotime((string)($article['published_at'] ?? '')) > time()) {
if (!hasCapability('view_previews')) {
http_response_code(404);
echo 'Article introuvable.';
include BASE_PATH . '/templates/404.php';
exit;
}
}
@@ -700,10 +719,33 @@ switch ($action) {
$isPrivateCat = $articleCat !== '' && in_array($articleCat, $privateCats, true);
if ($isPrivateCat && !isLoggedIn()) {
http_response_code(404);
echo 'Article introuvable.';
include BASE_PATH . '/templates/404.php';
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']);
// 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) ?? ''),
'/'
);
log404('/' . $notFoundPath);
$q = slugToSearchQuery($notFoundPath);
header('Location: /search' . ($q !== '' ? '?q=' . urlencode($q) : ''), true, 302);
exit;