diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b98386..9ba9bf2 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.16] - 2026-05-16
+
+### Ajouté
+- `SearchLogParser` : paramètre `$days` (7 ou 14) — cache distinct par période, filtre logFiles par date (#46)
+- `admin/searches` : boutons 7 j / 14 j pour choisir la fenêtre d'analyse (#46)
+
+### Modifié
+- `SearchLogParser` : tri par visiteurs uniques (IPs distinctes) au lieu de hits bruts — colonne renommée « Visiteurs » (#41)
+- URL inconnue / article introuvable : redirection 302 vers `/search?q=…` au lieu de page 404 (#57)
+- `edit_tags` : sections « Abréviations » et « Noms composés » masquées si des valeurs connues existent pour le type (#48)
+
+---
+
## [1.6.15] - 2026-05-16
### Ajouté
diff --git a/public/index.php b/public/index.php
index 37a305d..0cc6c77 100644
--- a/public/index.php
+++ b/public/index.php
@@ -50,12 +50,17 @@ $metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' :
unset($_noindexActions);
// ─── Recherche de l'article le plus proche et redirection 301 ────────────────
+function slugToSearchQuery(string $rawPath): string
+{
+ return trim((string)preg_replace('/\s{2,}/', ' ', (string)preg_replace(
+ '/[^a-zA-ZÀ-ÿ0-9\s]/u', ' ', str_replace(['-', '_', '/'], ' ', $rawPath)
+ )));
+}
+
function searchAndRedirect(string $rawPath, ArticleManager $articles): void
{
require_once BASE_PATH . '/src/SearchEngine.php';
- $query = (string)preg_replace('/\s{2,}/', ' ', trim(
- (string)preg_replace('/[^a-zA-ZÀ-ÿ0-9\s]/u', ' ', str_replace(['-', '_', '/'], ' ', $rawPath))
- ));
+ $query = slugToSearchQuery($rawPath);
if ($query === '') {
return;
}
@@ -666,9 +671,8 @@ switch ($action) {
case 'view':
$article = $slug !== '' ? $articles->getBySlug($slug) : null;
if (!$article) {
- searchAndRedirect($slug, $articles);
- http_response_code(404);
- echo 'Article introuvable.';
+ $q = slugToSearchQuery($slug);
+ header('Location: /search' . ($q !== '' ? '?q=' . urlencode($q) : ''), true, 302);
exit;
}
@@ -2577,9 +2581,11 @@ switch ($action) {
exit;
}
require_once BASE_PATH . '/src/SearchLogParser.php';
- $parser = new SearchLogParser('/var/log/apache2', apacheAccessLog());
- $adminData['search_terms'] = $parser->topTerms(100);
+ $days = in_array((int)($_GET['days'] ?? 14), [7, 14], true) ? (int)$_GET['days'] : 14;
+ $parser = new SearchLogParser('/var/log/apache2', apacheAccessLog(), '', 600, $days);
+ $adminData['search_terms'] = $parser->topTerms(100);
$adminData['search_log_readable'] = $parser->isReadable();
+ $adminData['search_days'] = $days;
}
if ($tab === 'stats') {
@@ -3383,23 +3389,9 @@ switch ($action) {
(string)(parse_url($_SERVER['REDIRECT_URL'] ?? $_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?? ''),
'/'
);
- if ($notFoundPath !== '') {
- searchAndRedirect(basename($notFoundPath), $articles);
- }
- http_response_code(404);
- ob_start();
- ?>
-
-
Page introuvable
-
Cette adresse ne correspond à aucun article.
Vous avez peut-être suivi un ancien lien.
-
← Retour à l'accueil
-
- logDir = rtrim($logDir, '/');
$this->vhostBase = $vhostBase;
+ $this->days = max(1, min(30, $days));
$this->cacheFile = $cacheFile !== ''
? $cacheFile
- : dirname(__DIR__) . '/_cache/search_terms.json';
+ : dirname(__DIR__) . '/_cache/search_terms_' . $this->days . 'd.json';
$this->cacheTtl = $cacheTtl;
}
- /** @return array terme => nombre d'occurrences, trié desc */
+ /** @return array terme => visiteurs uniques, trié desc */
public function topTerms(int $limit = 100): array
{
if ($this->cacheValid()) {
@@ -33,9 +36,14 @@ class SearchLogParser
}
}
- $counts = [];
+ $visitors = []; // terme => [ip => true]
foreach ($this->logFiles() as $file) {
- $this->parseFile($file, $counts);
+ $this->parseFile($file, $visitors);
+ }
+
+ $counts = [];
+ foreach ($visitors as $term => $ips) {
+ $counts[$term] = count($ips);
}
arsort($counts);
@@ -61,6 +69,7 @@ class SearchLogParser
{
$pattern = $this->logDir . '/' . $this->vhostBase;
$files = [];
+ $cutoff = time() - $this->days * 86400;
// Fichiers correspondant au pattern de base (courants + rotations incluses si glob)
$bases = glob($pattern) ?: [];
@@ -75,6 +84,9 @@ class SearchLogParser
if (!is_readable($path)) {
continue;
}
+ if (@filemtime($path) < $cutoff) {
+ continue;
+ }
if (str_ends_with($path, '.tar.gz')) {
$files[] = ['path' => $path, 'type' => 'tgz'];
} elseif (str_ends_with($path, '.gz')) {
@@ -88,7 +100,7 @@ class SearchLogParser
return $files;
}
- private function parseFile(array $file, array &$counts): void
+ private function parseFile(array $file, array &$visitors): void
{
if ($file['type'] === 'tgz') {
try {
@@ -99,7 +111,7 @@ class SearchLogParser
continue;
}
foreach (explode("\n", $content) as $line) {
- $this->parseLine($line, $counts);
+ $this->parseLine($line, $visitors);
}
}
} catch (\Exception $e) {
@@ -113,7 +125,7 @@ class SearchLogParser
while (!gzeof($h)) {
$line = gzgets($h, 8192);
if ($line !== false) {
- $this->parseLine($line, $counts);
+ $this->parseLine($line, $visitors);
}
}
gzclose($h);
@@ -123,28 +135,29 @@ class SearchLogParser
return;
}
while (($line = fgets($h)) !== false) {
- $this->parseLine($line, $counts);
+ $this->parseLine($line, $visitors);
}
fclose($h);
}
}
- private function parseLine(string $line, array &$counts): void
+ private function parseLine(string $line, array &$visitors): void
{
if (!str_contains($line, 'GET /search?')) {
return;
}
- if (!preg_match('/"GET \/search\?([^"]*) HTTP\//', $line, $m)) {
+ if (!preg_match('/^(\S+) \S+ \S+ \[[^\]]+\] "GET \/search\?([^"]*) HTTP\//', $line, $m)) {
return;
}
- parse_str($m[1], $params);
+ $ip = $m[1];
+ parse_str($m[2], $params);
$q = trim(urldecode($params['q'] ?? ''));
if ($q === '' || mb_strlen($q) > 200) {
return;
}
$q = mb_strtolower($q);
- $counts[$q] = ($counts[$q] ?? 0) + 1;
+ $visitors[$q][$ip] = true;
}
}
diff --git a/templates/admin.php b/templates/admin.php
index d0dedaf..6acda9b 100644
--- a/templates/admin.php
+++ b/templates/admin.php
@@ -1207,7 +1207,15 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
Termes recherchés
= count($adminData['search_terms'] ?? []) ?>
- Derniers 14 jours de logs · cache 10 min
+
+
Derniers = (int)($adminData['search_days'] ?? 14) ?> jours · cache 10 min
+
+
@@ -1228,7 +1236,7 @@ foreach (COLOR_PALETTE_16 as $_i => $_rgb):
| # |
Terme recherché |
- Fois |
+ Visiteurs |
|
diff --git a/templates/edit_tags.php b/templates/edit_tags.php
index 7621331..99007b2 100644
--- a/templates/edit_tags.php
+++ b/templates/edit_tags.php
@@ -97,8 +97,10 @@ $_typeLabel = $isCatField ? 'Catégorie' : ($tagTypes[$tagType] ?? ucfirst($tag
+
+
Aucun terme détecté dans cet article.