đ Documentation complĂšte du systĂšme de cache et du serveur de pages
Ce document récapitule l'intégralité du fonctionnement du serveur de pages PHP/Markdown, avec la gestion du cache, l'arborescence, et toutes les bonnes pratiques intégrées.
đșïž Architecture globale des fichiers
/ (Racine du site)
â
âââ .enableCache â đ© Fichier drapeau (flag). S'il est ABSENT â on vide le cache.
âââ .cache/ â đŠ Dossier contenant les pages HTML prĂ©-gĂ©nĂ©rĂ©es.
â âââ accueil.html (ex: e4d909c... gĂ©nĂ©rĂ© via md5)
â âââ contact.html
â
âââ index.php â đ Programme principal (lecteur de pages)
â
âââ pages/ â đ Dossier contenant vos sources Markdown
âââ index.php â âïž Programme de gĂ©nĂ©ration de l'arborescence
âââ listFiles.html â đł Arborescence HTML du site (le "menu")
âââ listFiles.hash â đ Empreinte numĂ©rique du dossier `pages/`
âââ accueil.md
âââ contact.md
âââ sous-dossier/
âââ exemple.md
đ§ DĂ©finitions des concepts-clĂ©s
| Concept | Explication simplifiée |
|---|---|
| Hash (ou empreinte numĂ©rique) | C'est une « signature » unique gĂ©nĂ©rĂ©e Ă partir du contenu d'un dossier. Si un seul fichier change (nom, date ou taille), la signature change complĂštement. Ăa permet de savoir instantanĂ©ment si la structure a Ă©tĂ© modifiĂ©e. |
Flag (.enableCache) |
C'est un simple interrupteur. S'il est prĂ©sent â le cache est "autorisĂ©" et stable. S'il est absent â cela signifie qu'une modification a eu lieu, il faut supprimer toutes les vieilles pages mises en cache. |
md5($slug) |
Fonction qui transforme le nom d'une page (ex: "mon-article") en une chaßne de caractÚres sécurisée (ex: "a5f3c..."). Cela évite les problÚmes avec les accents, les espaces ou les caractÚres spéciaux dans les noms de fichiers. |
| Cache individuel | Chaque page .md possÚde son propre fichier .html dans .cache/. On ne régénÚre ce fichier que si la page .md est plus récente que le fichier .html (comparaison des dates). |
âïž Programme n°1 : Le gĂ©nĂ©rateur d'arborescence (/pages/index.php)
RĂŽle : Surveiller le dossier pages/ et mettre Ă jour le menu du site.
đ Logique pas Ă pas
- Scanne récursivement tout le dossier
pages/. - Calcule le hash actuel du dossier en prenant en compte :
- Le chemin de chaque fichier.
- Sa taille.
- Sa date de derniĂšre modification.
- Compare ce hash avec l'ancien hash stocké dans
listFiles.hash. - Si le hash a changé (ou si
listFiles.htmln'existe pas) :- đ On reconstruit entiĂšrement l'arborescence HTML.
- đ On Ă©crase
listFiles.htmlavec la nouvelle version. - đŸ On enregistre le nouveau hash dans
listFiles.hash. - đ© Action critique : On supprime le fichier
.enableCache(situé à la racine du site) pour signaler au programme principal que la structure a bougé.
đ Code complet de /pages/index.php
<?php
/**
* pages/index.php â GĂ©nĂ©rateur d'arborescence avec hash
*/
$extParam = 'md';
$allowedExtensions = array_filter(array_map('trim', explode(',', strtolower($extParam))));
$root = __DIR__; // dossier /pages
$fichierHtml = $root . '/listFiles.html';
$fichierHash = $root . '/listFiles.hash';
/* ============================================================
FONCTION : Calcul du hash du dossier
============================================================ */
function hashDirectory($dir) {
$hashes = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
$hashes[] =
$file->getPathname() . '|' .
$file->getSize() . '|' .
$file->getMTime();
}
sort($hashes);
return md5(implode("\n", $hashes));
}
/* ============================================================
FONCTION : Construction de l'arborescence HTML
============================================================ */
function buildTree($dir, $extensions, $root, $htmlName) {
$items = scandir($dir);
$files = $folders = [];
foreach ($items as $i) {
if ($i[0] === '.' || $i === $htmlName) continue;
$p = $dir . '/' . $i;
if (is_dir($p)) {
$folders[] = $i;
} else {
$e = strtolower(pathinfo($i, PATHINFO_EXTENSION));
if (in_array($e, $extensions)) {
$files[] = $i;
}
}
}
natcasesort($files);
natcasesort($folders);
$h = "<ul>\n";
/* --- Fichiers --- */
foreach ($files as $f) {
$full = $dir . '/' . $f;
$rel = ltrim(str_replace($root, '', $full), '/');
$info = pathinfo($f);
$href = ($info['extension'] === 'md')
? substr($rel, 0, -3)
: $rel;
$label = ucfirst(str_replace('-', ' ', $info['filename']));
$h .= " <li><a href=\"?page=" . htmlspecialchars($href) . "\">"
. htmlspecialchars($label)
. "</a></li>\n";
}
/* --- Dossiers --- */
foreach ($folders as $fod) {
$sub = buildTree($dir . '/' . $fod, $extensions, $root, $htmlName);
if ($sub !== "<ul>\n</ul>\n") {
$h .= " <li><details><summary>"
. ucfirst($fod)
. "</summary>"
. $sub
. "</details></li>\n";
}
}
return $h . "</ul>\n";
}
/* ============================================================
LOGIQUE PRINCIPALE : Comparaison des hashs
============================================================ */
$currentHash = hashDirectory($root);
$oldHash = file_exists($fichierHash) ? trim(file_get_contents($fichierHash)) : '';
if (!file_exists($fichierHtml) || $currentHash !== $oldHash) {
// 1. Régénération de l'arborescence
$menu = buildTree($root, $allowedExtensions, $root, basename($fichierHtml));
$menu .= "<!-- Généré le : " . date('Y-m-d H:i:s') . " -->\n";
file_put_contents($fichierHtml, $menu);
file_put_contents($fichierHash, $currentHash);
// 2. SUPPRESSION DU FLAG .enableCache (pour vider le cache global)
$flagFile = $root . '/../.enableCache';
if (file_exists($flagFile)) {
unlink($flagFile);
}
}
// 3. Retourne le contenu du menu pour l'affichage (ou l'inclusion)
return file_get_contents($fichierHtml);
?>
đ Programme n°2 : Le serveur de pages (/index.php Ă la racine)
RÎle : Afficher la page demandée par l'utilisateur (?page=accueil) en utilisant le cache.
đ Logique pas Ă pas
-
Vérifie l'existence du flag
.enableCache:- â Si le flag est ABSENT :
- On supprime TOUS les fichiers
.htmldu dossier.cache/. - On recrée immédiatement le flag
.enableCache(pour éviter que le prochain visiteur ne supprime à nouveau).
- On supprime TOUS les fichiers
- â
Si le flag est PRĂSENT : On ne touche pas au dossier
.cache/.
- â Si le flag est ABSENT :
-
RécupÚre le nom de la page via
$_GET['page'](ex:accueil). -
Sécurise le nom (
basename()pour éviter les attaques type../). -
Définit les chemins :
- Source :
pages/accueil.md - Cache :
.cache/md5(accueil).html
- Source :
-
Vérifie si le cache individuel est valide :
- Si le fichier
.htmln'existe pas â on le gĂ©nĂšre. - Si le fichier
.mdest plus rĂ©cent que le.htmlâ on le rĂ©gĂ©nĂšre.
- Si le fichier
-
Affiche le contenu du fichier
.html(cache).
đ Code complet de /index.php (racine)
<?php
session_start();
// 1. Configuration des dossiers
$cachePath = __DIR__ . '/.cache/'; // SLASH OBLIGATOIRE !
$pagesDir = __DIR__ . "/pages";
$defaultPage = "accueil";
$assetsRoot = __DIR__ . '/assets';
// 2. Fichier de configuration
$configFile = __DIR__ . '/config.ini';
if (!file_exists($configFile)) {
die("Erreur : config.ini introuvable");
}
$config = parse_ini_file($configFile, true);
// 3. Vérification des dossiers
if (!is_dir($pagesDir)) die("Erreur : dossier pages introuvable");
if (!is_dir($assetsRoot)) die("Erreur : dossier assets introuvable");
// 4. Sécurisation de la page demandée
$page = $_GET['page'] ?? '';
$page = trim($page);
$page = str_replace(["..", "\\"], "", $page);
$page = preg_replace('/[^a-zA-Z0-9\/\-_]/', '', $page);
if ($page === '') {
$page = $defaultPage;
}
$file = "$pagesDir/$page.md";
if (!is_file($file)) {
$page = $defaultPage;
$file = "$pagesDir/$defaultPage.md";
}
// 5. GESTION DU CACHE GLOBAL (flag .enableCache)
$flagFile = __DIR__ . '/.enableCache';
// Créer le dossier cache s'il n'existe pas
if (!is_dir($cachePath)) {
mkdir($cachePath, 0755, true);
}
// Si le flag est ABSENT, on vide tout le cache et on le recrée
if (!file_exists($flagFile)) {
$cacheFiles = glob($cachePath . '*.html');
foreach ($cacheFiles as $f) {
if (is_file($f)) unlink($f);
}
touch($flagFile); // recréation immédiate
}
// Déterminer si le cache est activé (flag présent)
$enableCache = file_exists($flagFile);
// 6. GESTION DU CACHE INDIVIDUEL (par page)
$cacheFile = $cachePath . md5($page) . '.html';
$cacheValid = false;
// Vérifier si le cache individuel existe et est à jour
if ($enableCache && file_exists($cacheFile)) {
$mdTime = filemtime($file);
$cacheTime = filemtime($cacheFile);
if ($cacheTime >= $mdTime) {
$cacheValid = true;
}
}
// Si le cache est valide, on l'affiche et on arrĂȘte le script
if ($cacheValid) {
// Envoyer les en-tĂȘtes HTTP pour le cache navigateur
header('Cache-Control: public, max-age=86400');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
readfile($cacheFile);
exit;
}
// 7. CHARGEMENT DU CONTENU MARKDOWN
$markdown = file_get_contents($file);
// 8. EXTRACTION DES MĂTADONNĂES
$metadata = [];
if (preg_match('/<!--\s*metadata\s*(.*?)-->/s', $markdown, $matches)) {
$metaBlock = trim($matches[1]);
$metadata = parse_ini_string($metaBlock);
}
// Fusion avec la configuration
$config = array_merge($config, $metadata);
// Extraction des valeurs (SANS extract() pour la sécurité)
$title = $config['title'] ?? 'Titre par défaut';
$description = $config['description'] ?? '';
$keywords = $config['keywords'] ?? '';
$author = $config['author'] ?? 'Auteur inconnu';
$siteName = $config['siteName'] ?? 'Mon Site';
$siteUrl = $config['siteUrl'] ?? '/';
// 9. GĂNĂRATION DU MENU
$menuHtml = include __DIR__ . '/pages/index.php';
$menuHtmlOpen = treeviewSetOpen($menuHtml, true);
// 10. TRAITEMENT DU MARKDOWN (variables + syntaxe personnalisée)
$toutes_mes_vars = get_defined_vars();
$markdown = replace_vars($markdown, $toutes_mes_vars);
$markdown = parseCustomMarkdown($markdown);
// 11. CONVERSION MARKDOWN -> HTML
require_once $assetsRoot . '/libs/Parsedown.php';
require_once $assetsRoot . '/libs/ParsedownExtra.php';
require_once $assetsRoot . '/libs/ParsedownExtraPlugin.php';
$Parsedown = new ParsedownExtraPlugin();
$content = $Parsedown->text($markdown);
// 12. CONSTRUCTION DE LA PAGE COMPLĂTE
$cssVersion = filemtime($_SERVER['DOCUMENT_ROOT'] . '/assets/css/style.css');
$pageComplete = <<<HTML
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> {$siteName} | {$title} </title>
<meta name="description" content="{$description}">
<meta name="keywords" content="{$keywords}">
<meta name="author" content="{$author}">
<link rel="stylesheet" href="/assets/css/style.css?v=$cssVersion">
</head>
<body>
<button id="hamburger" onclick="toggleMenu()">â°</button>
<div id="sidebar" class="sidebar treeview">
<div class="menu-navigation">
{$menuHtml}
</div>
</div>
<div id="main" class="content" onclick="closeMenu()">
{$content}
<?php if (!empty(\$_COOKIE['admin'])): ?>
<button class="btn-admin" onclick="window.location.href='admin.php?page=<?= urlencode(\$page) ?>'">
Administration
</button>
<button class="btn-logout" onclick="window.location.href='admin.php?logout=1'">
Déconnexion
</button>
<?php endif; ?>
</div>
<script>
function toggleMenu(){document.getElementById("sidebar").classList.toggle("open");}
function closeMenu(){document.getElementById("sidebar").classList.remove("open");}
</script>
</body>
</html>
HTML;
// 13. ĂCRITURE DU CACHE
if ($enableCache) {
$result = file_put_contents($cacheFile, $pageComplete);
if ($result === false) {
// Option : log silencieux
// die("Ăchec Ă©criture cache : $cacheFile");
}
}
// 14. ENVOI DES EN-TĂTES ET AFFICHAGE
header('Cache-Control: public, max-age=86400');
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
echo $pageComplete;
// ============================================================
// FONCTIONS UTILITAIRES
// ============================================================
function treeviewSetOpen(string $html, bool $open = true): string {
if ($open) {
$html = preg_replace('/<details(?![^>]*\bopen\b)/', '<details open', $html);
} else {
$html = preg_replace('/<details\s+open/', '<details', $html);
}
return $html;
}
function replace_vars(string $md, array $vars): string {
if (strpos($md, '{{') === false || strpos($md, '}}') === false) {
return $md;
}
foreach ($vars as $key => $value) {
if (is_array($value) || is_object($value)) continue;
$pattern = '/\{\{' . preg_quote($key, '/') . '\}\}/';
$md = preg_replace($pattern, (string)$value, $md);
}
return $md;
}
function parseCustomMarkdown($html) {
// Syntaxe <span class="...)) -> span $regexInline = '/\(\(\s*([^\">]+?)\s*\|\s*([\s\S]*?)\s*\)\)/';
$html = preg_replace_callback($regexInline, function($matches) {
$attributsBruts = trim($matches[1]);
$texteInterieur = trim($matches[2]);
$attributsNettoyes = preg_replace('/\s+/', ' ', $attributsBruts);
return '<span ' . $attributsNettoyes . '>' . $texteInterieur . '</span>';
}, $html);
// Syntaxe <div class="...</span>) -> div"><div class="box-contenu" markdown="1">
$regexBloc = '/\(\(\(\s*([a-z]*)\s*([\s\S]*?)\)\)\)/';
$html = preg_replace_callback($regexBloc, function($matches) {
$type = trim($matches[1]);
$contenu = trim($matches[2]);
$attributType = ($type === '') ? 'defaultBox' : $type;
return '<div box ' . $attributType . '><div class="box-contenu" markdown="1">' . "\n" . $contenu . "\n" . '</div></div>';
}, $html);
return $html;
}
?>
đ Cycle de vie complet (ScĂ©narios concrets)
Scénario A : L'administrateur ajoute une nouvelle page
- Admin crée
pages/nouveau.md(attention Ă l'extension.mden minuscules !). - L'admin rafraĂźchit le menu (ou appelle
pages/index.php). pages/index.phpcalcule le hash â diffĂ©rent de l'ancien.- Il rĂ©gĂ©nĂšre
listFiles.htmlet supprime.enableCache. - Premier visiteur arrive sur le site.
index.phpvoit que.enableCacheest absent â vide tout.cache/â recrĂ©e.enableCache.- Le visiteur demande
?page=nouveauâ le fichier.htmln'existe pas, il est gĂ©nĂ©rĂ© et affichĂ©. - Visites suivantes â le flag existe â les pages sont servies en cache sans aucune suppression.
Scénario B : L'administrateur modifie le contenu d'une page (sans renommer)
- Admin modifie
pages/accueil.md. - L'admin rafraĂźchit le menu.
pages/index.phpcalcule le hash â change (car la date de modification a changĂ©).- Il rĂ©gĂ©nĂšre
listFiles.htmlet supprime.enableCache. - Premier visiteur arrive â le flag est absent â vide tout
.cache/(mĂȘme les pages qui n'ont pas changĂ©). - Le visiteur demande
accueilâ comme le cache est vidĂ©, il est rĂ©gĂ©nĂ©rĂ© avec le nouveau contenu. - Les autres pages sont rĂ©gĂ©nĂ©rĂ©es au fur et Ă mesure des demandes.
Scénario C : Aucune modification (cas nominal)
- Le flag
.enableCacheest présent. - Le hash dans
pages/listFiles.hashcorrespond au dossier actuel. index.phplit directement le fichier.cache/accueil.html.- ZĂ©ro Ă©criture disque, performances maximales đ.
đ§ Pourquoi cette architecture est-elle robuste ?
| ĂlĂ©ment | Avantage |
|---|---|
| Hash du dossier | Détecte toute modification (ajout, suppression, renommage, modification de contenu). Bien plus fiable qu'une simple vérification de date. |
Flag .enableCache |
Ăvite de recalculer/supprimer le cache Ă chaque requĂȘte. La suppression n'a lieu qu'une seule fois juste aprĂšs une modification. |
md5($slug) |
Rend les noms de fichiers compatibles avec tous les systÚmes d'exploitation (pas d'accents, de caractÚres interdits ou de traversées de répertoire). |
| Vidage total du cache | SimplicitĂ© extrĂȘme. Comme il n'y a qu'une centaine de fichiers, la purge est quasi instantanĂ©e. Aucun risque de laisser des fichiers orphelins. |
â ïž Rappels importants pour l'avenir
- Ne jamais recréer
.enableCacheailleurs que dansindex.php(sinon le systÚme de vidange ne se déclenchera pas). - Le hash est roi : si jamais
listFiles.htmlest obsolĂšte, c'est toujours le hash qui dĂ©cidera de le regĂ©nĂ©rer. - Le cache individuel se base sur les dates : mĂȘme si tu modifies une page et que le flag est prĂ©sent, le cache de cette page sera mis Ă jour automatiquement (car la date du
.mddevient plus récente que celle du.html). - Pense à inclure Parsedown : télécharge-le ou installe-le via Composer (
composer require erusev/parsedown) et ajuste lerequire_oncedansindex.php. - Les nouveaux fichiers doivent avoir l'extension
.md(et non.HTML,.html,.txt, etc.) pour ĂȘtre reconnus parbuildTree. - Le chemin du cache doit ĂȘtre
__DIR__ . '/.cache/'(avec le slash) pour que les fichiers soient correctement enregistrés.
đ ïž DĂ©pannage rapide
| ProblĂšme | Solution |
|---|---|
| Les nouveaux fichiers n'apparaissent pas dans le menu | VĂ©rifie l'extension du fichier (doit ĂȘtre .md). Supprime listFiles.html et listFiles.hash pour forcer la rĂ©gĂ©nĂ©ration. |
| Les pages ne sont pas mises en cache | Vérifie que le flag .enableCache existe. Supprime-le pour forcer la recréation du cache. |
| Le chemin du cache ne fonctionne pas | Vérifie que $cachePath = __DIR__ . '/.cache/'; contient bien le slash avant .cache. |
| Erreur "headers already sent" | Déplace les header() avant tout echo ou readfile. |
| Le menu est vide | Vérifie que pages/index.php existe et contient bien return file_get_contents($fichierHtml); à la fin. |
âïž Fichier créé pour mĂ©moire â En cas de doute, relis cette doc avant de toucher aux dossiers
.cacheoupages/.
đ Pour copier ce texte facilement
Si la copie ne fonctionne pas, essaye ces astuces :
- Sélectionne tout avec Ctrl+A (ou Cmd+A sur Mac) puis Ctrl+C (ou Cmd+C).
- Utilise le bouton de copie en haut Ă droite du bloc de code (si ton interface l'affiche).
- Copie bloc par bloc en cliquant trois fois sur chaque section pour la sélectionner.
Normalement, le texte devrait ĂȘtre copiable. Si le problĂšme persiste, dis-moi sur quel appareil/navigateur tu es, je pourrai te proposer une solution alternative. đ
Ah pardon, jâai compris !
Tu veux un seul bloc MD prĂȘt Ă copier-coller sans texte autour.
Je te le refais proprement en utilisant ~~~ pour dĂ©limiter le document complet, et Ă lâintĂ©rieur, chaque morceau de code PHP est bien fermĂ© avec ses propres php ... .
Voici le fichier final, prĂȘt Ă ĂȘtre enregistrĂ© sous DOCUMENTATION_CACHE.md :
đ Documentation complĂšte du systĂšme de cache (PHP / Markdown)
Objectif : Servir des pages
.md(converties en HTML) rapidement, tout en réagissant intelligemment aux modifications de la structure des dossiers.
đșïž Architecture globale des fichiers
/ (Racine du site)
â
âââ .enableCache â đ© Fichier drapeau (flag). S'il est ABSENT â on vide le cache.
âââ .cache/ â đŠ Dossier contenant les pages HTML prĂ©-gĂ©nĂ©rĂ©es.
â âââ accueil.html (ex: e4d909c... gĂ©nĂ©rĂ© via md5)
â âââ contact.html
â
âââ index.php â đ Programme principal (lecteur de pages)
â
âââ pages/ â đ Dossier contenant vos sources Markdown
âââ index.php â âïž Programme de gĂ©nĂ©ration de l'arborescence
âââ listFiles.html â đł Arborescence HTML du site (le "menu")
âââ listFiles.hash â đ Empreinte numĂ©rique du dossier `pages/`
âââ accueil.md
âââ contact.md
âââ sous-dossier/
âââ exemple.md
đ§ DĂ©finitions des concepts-clĂ©s (pour ne pas oublier)
| Concept | Explication simplifiée |
|---|---|
| Hash (ou empreinte numĂ©rique) | C'est une « signature » unique gĂ©nĂ©rĂ©e Ă partir du contenu d'un dossier. Si un seul fichier change (nom, date ou taille), la signature change complĂštement. Ăa permet de savoir instantanĂ©ment si la structure a Ă©tĂ© modifiĂ©e. |
Flag (.enableCache) |
C'est un simple interrupteur. S'il est prĂ©sent â le cache est "autorisĂ©" et stable. S'il est absent â cela signifie qu'une modification a eu lieu, il faut supprimer toutes les vieilles pages mises en cache. |
md5($slug) |
Fonction qui transforme le nom d'une page (ex: "mon-article") en une chaßne de caractÚres sécurisée (ex: "a5f3c..."). Cela évite les problÚmes avec les accents, les espaces ou les caractÚres spéciaux dans les noms de fichiers. |
| Cache individuel | Chaque page .md possÚde son propre fichier .html dans .cache/. On ne régénÚre ce fichier que si la page .md est plus récente que le fichier .html (comparaison des dates). |
âïž Programme n°1 : Le gĂ©nĂ©rateur d'arborescence (/pages/index.php)
RĂŽle : Surveiller le dossier pages/ et mettre Ă jour le menu du site.
đ Logique pas Ă pas
- Scanne récursivement tout le dossier
pages/. - Calcule le hash actuel du dossier en prenant en compte :
- Le chemin de chaque fichier.
- Sa taille.
- Sa date de derniĂšre modification.
- Compare ce hash avec l'ancien hash stocké dans
listFiles.hash. - Si le hash a changé (ou si
listFiles.htmln'existe pas) :- đ On reconstruit entiĂšrement l'arborescence HTML.
- đ On Ă©crase
listFiles.htmlavec la nouvelle version. - đŸ On enregistre le nouveau hash dans
listFiles.hash. - đ© Action critique : On supprime le fichier
.enableCache(situé à la racine du site) pour signaler au programme principal que la structure a bougé.
đ Code complet de /pages/index.php
<?php
/**
* pages/index.php â GĂ©nĂ©rateur d'arborescence avec hash
*/
$extParam = 'md';
$allowedExtensions = array_filter(array_map('trim', explode(',', strtolower($extParam
</div></div>);
$root = __DIR__; // dossier /pages
$fichierHtml = $root . '/listFiles.html';
$fichierHash = $root . '/listFiles.hash';
/* ============================================================
FONCTION : Calcul du hash du dossier
============================================================ */
function hashDirectory($dir) {
$hashes = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
$hashes[] =
$file->getPathname() . '|' .
$file->getSize() . '|' .
$file->getMTime();
}
sort($hashes);
return md5(implode("\n", $hashes));
}
/* ============================================================
FONCTION : Construction de l'arborescence HTML
============================================================ */
function buildTree($dir, $extensions, $root, $htmlName) {
$items = scandir($dir);
$files = $folders = [];
foreach ($items as $i) {
if ($i[0] === '.' || $i === $htmlName) continue;
$p = $dir . '/' . $i;
if (is_dir($p)) {
$folders[] = $i;
} else {
$e = strtolower(pathinfo($i, PATHINFO_EXTENSION));
if (in_array($e, $extensions)) {
$files[] = $i;
}
}
}
natcasesort($files);
natcasesort($folders);
$h = "<ul>\n";
/* --- Fichiers --- */
foreach ($files as $f) {
$full = $dir . '/' . $f;
$rel = ltrim(str_replace($root, '', $full), '/');
$info = pathinfo($f);
$href = ($info['extension'] === 'md')
? substr($rel, 0, -3)
: $rel;
$label = ucfirst(str_replace('-', ' ', $info['filename']));
$h .= " <li><a href=\"?page=" . htmlspecialchars($href) . "\">"
. htmlspecialchars($label)
. "</a></li>\n";
}
/* --- Dossiers --- */
foreach ($folders as $fod) {
$sub = buildTree($dir . '/' . $fod, $extensions, $root, $htmlName);
if ($sub !== "<ul>\n</ul>\n") {
$h .= " <li><details><summary>"
. ucfirst($fod)
. "</summary>"
. $sub
. "</details></li>\n";
}
}
return $h . "</ul>\n";
}
/* ============================================================
LOGIQUE PRINCIPALE : Comparaison des hashs
============================================================ */
$currentHash = hashDirectory($root);
$oldHash = file_exists($fichierHash) ? trim(file_get_contents($fichierHash)) : '';
if (!file_exists($fichierHtml) || $currentHash !== $oldHash) {
// 1. Régénération de l'arborescence
$menu = buildTree($root, $allowedExtensions, $root, basename($fichierHtml));
$menu .= "<!-- Généré le : " . date('Y-m-d H:i:s') . " -->\n";
file_put_contents($fichierHtml, $menu);
file_put_contents($fichierHash, $currentHash);
// 2. SUPPRESSION DU FLAG .enableCache (pour vider le cache global)
$flagFile = $root . '/../.enableCache';
if (file_exists($flagFile)) {
unlink($flagFile);
}
}
// 3. Retourne le contenu du menu pour l'affichage (ou l'inclusion)
return file_get_contents($fichierHtml);
?>
đ Programme n°2 : Le serveur de pages (/index.php Ă la racine)
RÎle : Afficher la page demandée par l'utilisateur (?page=accueil) en utilisant le cache.
đ Logique pas Ă pas
-
Vérifie l'existence du flag
.enableCache:- â Si le flag est ABSENT :
- On supprime TOUS les fichiers
.htmldu dossier.cache/. - On recrée immédiatement le flag
.enableCache(pour éviter que le prochain visiteur ne supprime à nouveau).
- On supprime TOUS les fichiers
- â
Si le flag est PRĂSENT : On ne touche pas au dossier
.cache/.
- â Si le flag est ABSENT :
-
RécupÚre le nom de la page via
$_GET['page'](ex:accueil). -
Sécurise le nom (
basename()pour éviter les attaques type../). -
Définit les chemins :
- Source :
pages/accueil.md - Cache :
.cache/md5(accueil).html
- Source :
-
Vérifie si le cache individuel est valide :
- Si le fichier
.htmln'existe pas â on le gĂ©nĂšre. - Si le fichier
.mdest plus rĂ©cent que le.htmlâ on le rĂ©gĂ©nĂšre.
- Si le fichier
-
Affiche le contenu du fichier
.html(cache).
đ Code complet de /index.php (racine)
<?php
// 1. Inclusion de Parsedown (Ă adapter selon ton installation)
require_once 'Parsedown.php';
// 2. Configuration des dossiers
$root = __DIR__;
$pagesDir = $root . '/pages';
$cacheDir = $root . '/.cache';
$flagFile = $root . '/.enableCache';
// 3. Création du dossier cache s'il n'existe pas
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
// 4. GESTION DU FLAG GLOBAL : si absent, on vide tout le cache
if (!file_exists($flagFile)) {
$cacheFiles = glob($cacheDir . '/*.html');
foreach ($cacheFiles as $file) {
if (is_file($file)) {
unlink($file);
}
}
// RECRĂER IMMĂDIATEMENT le flag pour Ă©viter une boucle
touch($flagFile);
}
// 5. Récupération et sécurisation de la page demandée
$page = isset($_GET['page']) ? $_GET['page'] : 'accueil';
$page = basename($page); // Supprime tout chemin relatif
$mdFile = $pagesDir . '/' . $page . '.md';
$cacheFile = $cacheDir . '/' . md5($page) . '.html';
// 6. Vérification que la source Markdown existe
if (!file_exists($mdFile)) {
die('Page introuvable.');
}
// 7. Gestion du cache individuel (basé sur les dates)
$regenerate = true;
if (file_exists($cacheFile)) {
$mdTime = filemtime($mdFile);
$cacheTime = filemtime($cacheFile);
if ($cacheTime >= $mdTime) {
$regenerate = false; // Le cache est Ă jour
}
}
// 8. Régénération du cache individuel si nécessaire
if ($regenerate) {
$markdown = file_get_contents($mdFile);
$parsedown = new Parsedown();
$html = $parsedown->text($markdown);
// Encapsulation dans une page HTML complĂšte (optionnel)
$html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><title>$page</title></head><body>$html</body></html>";
file_put_contents($cacheFile, $html);
}
// 9. Affichage du cache
readfile($cacheFile);
?>
đ Cycle de vie complet (ScĂ©narios concrets)
Scénario A : L'administrateur ajoute une nouvelle page
- Admin crée
pages/nouveau.md. - L'admin rafraĂźchit le menu (ou appelle
pages/index.php). pages/index.phpcalcule le hash â diffĂ©rent de l'ancien.- Il rĂ©gĂ©nĂšre
listFiles.htmlet supprime.enableCache. - Premier visiteur arrive sur le site.
index.phpvoit que.enableCacheest absent â vide tout.cache/â recrĂ©e.enableCache.- Le visiteur demande
?page=nouveauâ le fichier.htmln'existe pas, il est gĂ©nĂ©rĂ© et affichĂ©. - Visites suivantes â le flag existe â les pages sont servies en cache sans aucune suppression.
Scénario B : L'administrateur modifie le contenu d'une page (sans renommer)
- Admin modifie
pages/accueil.md. - L'admin rafraĂźchit le menu.
pages/index.phpcalcule le hash â change (car la date de modification a changĂ©).- Il rĂ©gĂ©nĂšre
listFiles.htmlet supprime.enableCache. - Premier visiteur arrive â le flag est absent â vide tout
.cache/(mĂȘme les pages qui n'ont pas changĂ©). - Le visiteur demande
accueilâ comme le cache est vidĂ©, il est rĂ©gĂ©nĂ©rĂ© avec le nouveau contenu. - Les autres pages sont rĂ©gĂ©nĂ©rĂ©es au fur et Ă mesure des demandes.
Scénario C : Aucune modification (cas nominal)
- Le flag
.enableCacheest présent. - Le hash dans
pages/listFiles.hashcorrespond au dossier actuel. index.phplit directement le fichier.cache/accueil.html.- ZĂ©ro Ă©criture disque, performances maximales đ.
đ§ Pourquoi cette architecture est-elle robuste ?
| ĂlĂ©ment | Avantage |
|---|---|
| Hash du dossier | Détecte toute modification (ajout, suppression, renommage, modification de contenu). Bien plus fiable qu'une simple vérification de date. |
Flag .enableCache |
Ăvite de recalculer/supprimer le cache Ă chaque requĂȘte. La suppression n'a lieu qu'une seule fois juste aprĂšs une modification. |
md5($slug) |
Rend les noms de fichiers compatibles avec tous les systÚmes d'exploitation (pas d'accents, de caractÚres interdits ou de traversées de répertoire). |
| Vidage total du cache | SimplicitĂ© extrĂȘme. Comme il n'y a qu'une centaine de fichiers, la purge est quasi instantanĂ©e. Aucun risque de laisser des fichiers orphelins. |
â ïž Rappels importants pour l'avenir
- Ne jamais recréer
.enableCacheailleurs que dansindex.php(sinon le systÚme de vidange ne se déclenchera pas). - Le hash est roi : si jamais
listFiles.htmlest obsolĂšte, c'est toujours le hash qui dĂ©cidera de le regĂ©nĂ©rer. - Le cache individuel se base sur les dates : mĂȘme si tu modifies une page et que le flag est prĂ©sent, le cache de cette page sera mis Ă jour automatiquement (car la date du
.mddevient plus récente que celle du.html). - Pense à inclure Parsedown : télécharge-le ou installe-le via Composer (
composer require erusev/parsedown) et ajuste lerequire_oncedansindex.php.
âïž Fichier créé pour mĂ©moire â En cas de doute, relis cette doc avant de toucher aux dossiers
.cacheoupages/.
VoilĂ , un seul bloc prĂȘt Ă ĂȘtre copiĂ©-collĂ© dans ton Ă©diteur et enregistrĂ© en .md. Plus rien Ă l'extĂ©rieur. đ
đ Guide d'amĂ©lioration vers un serveur professionnel
Ce document liste les actions concrĂštes pour faire passer ton serveur de pages (PHP / Markdown) dâun outil personnel Ă une application digne dâun environnement professionnel.
đŻ Objectifs
- Sécurité renforcée (protection des données, des fichiers, des injections)
- Performance optimale (réduction de la latence, mise en cache avancée)
- Maintenabilité (code clair, tests, documentation)
- ScalabilitĂ© (prĂȘt Ă gĂ©rer des milliers de pages et des pics de trafic)
- Déploiement fiable (environnements dev/staging/prod, rollback)
1ïžâŁ SĂ©curitĂ© â PrioritĂ© absolue
đ Validation stricte des chemins
Actuel :
str_replace(["..", "\\"], "", $page) + regex.
ProblĂšme :
Peut ĂȘtre contournĂ© par des combinaisons comme .../.../etc/passwd.
Solution :
Utiliser realpath() pour rĂ©soudre le chemin complet et vĂ©rifier quâil reste dans le dossier pages/.
// Ă placer aprĂšs le nettoyage de $page
$page = $_GET['page'] ?? '';
$page = preg_replace('/[^a-zA-Z0-9\/\-_]/', '', $page);
$page = trim($page, '/');
if ($page === '') {
$page = $defaultPage;
}
$file = realpath($pagesDir . '/' . $page . '.md');
$pagesDirReal = realpath($pagesDir);
if ($file === false || strpos($file, $pagesDirReal) !== 0) {
http_response_code(404);
die('Page introuvable.');
}
// Ensuite, utiliser $file comme chemin sûr
đĄïž Ăviter extract() sur des donnĂ©es utilisateur
Actuel :
extract($config) aprÚs fusion avec les métadonnées.
Risque :
Injection de variables, écrasement de variables sensibles.
Solution :
Accéder directement aux clés avec des valeurs par défaut.
// Remplacer extract($config); et $title = $config['title']; etc.
$title = $config['title'] ?? 'Titre par défaut';
$description = $config['description'] ?? '';
$keywords = $config['keywords'] ?? '';
$author = $config['author'] ?? 'Auteur inconnu';
đ ProtĂ©ger les cookies (admin)
Actuel :
Cookie admin en clair.
Risque :
Vol de session si le site nâest pas en HTTPS.
Solution :
- Forcer HTTPS en production (
$_SERVER['HTTPS']). - Utiliser des cookies
SecureetHttpOnly. - Stocker un hash du mot de passe, jamais le mot de passe en clair.
setcookie('admin', $hash, [
'expires' => time() + 3600,
'secure' => true,
'httponly' => true,
'samesite' => 'Strict'
]);
đ Protection des fichiers sensibles
Dossiers à protéger : .cache/, pages/, assets/ (si contient des fichiers PHP).
Solution :
Placer un .htaccess (Apache) ou un fichier web.config (IIS) pour interdire lâaccĂšs direct.
# .htaccess dans .cache/
Deny from all
Ou mieux : déplacer le cache hors de la racine web :
$cachePath = '/var/www/cache/' . $_SERVER['HTTP_HOST'] . '/';
2ïžâŁ Performance â AccĂ©lĂ©rer le rendu
⥠Mise en cache HTTP (cache navigateur)
Actuel :
Le cache est uniquement serveur.
Ajout :
Envoyer des en-tĂȘtes HTTP pour que le navigateur mette en cache les ressources statiques (CSS, JS, images).
// Dans index.php, aprĂšs avoir lu le cache
header('Cache-Control: public, max-age=86400'); // 24h
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 86400) . ' GMT');
đ§č Nettoyer les fichiers obsolĂštes
Actuel :
Les fichiers *.html sâaccumulent dans .cache/.
Solution :
Ajouter une tùche planifiée (cron) pour supprimer les fichiers vieux de plus de 7 jours.
# Cron quotidien
find /chemin/.cache -name "*.html" -mtime +7 -delete
đŠ Mise en cache du menu (HTML)
Actuel :
Le menu est inclus Ă chaque page via include __DIR__ . '/pages/index.php';.
Optimisation :
Mettre en cache le HTML du menu dans un fichier séparé (ex: menu.html) et ne le régénérer que si le hash change.
$menuCache = $cachePath . 'menu.html';
if (file_exists($menuCache) && filemtime($menuCache) >= filemtime($pagesDir . '/listFiles.hash')) {
$menuHtml = file_get_contents($menuCache);
} else {
$menuHtml = include __DIR__ . '/pages/index.php';
file_put_contents($menuCache, $menuHtml);
}
3ïžâŁ Architecture et maintenabilitĂ©
đ§© Utiliser un autoloader (Composer)
Actuel :
require_once manuels pour Parsedown.
Amélioration :
Utiliser Composer pour gérer les dépendances et charger automatiquement les classes.
composer require erusev/parsedown
require_once __DIR__ . '/vendor/autoload.php';
đïž Structurer en classes (POO)
Actuel :
Code procédural avec des fonctions éparses.
Ăvolution :
Créer des classes :
PageManager(chargement, mĂ©tadonnĂ©es, cache)MenuBuilder(gĂ©nĂ©ration de lâarborescence)MarkdownRenderer(Parsedown + extensions)CacheManager(gestion du cache global et individuel)
Exemple de squelette :
class PageManager {
private $pagesDir;
private $cacheDir;
private $defaultPage;
public function __construct($pagesDir, $cacheDir, $defaultPage) {
$this->pagesDir = $pagesDir;
$this->cacheDir = $cacheDir;
$this->defaultPage = $defaultPage;
}
public function loadPage($slug) {
// ...
}
public function render($slug) {
// ...
}
}
đ Centraliser la configuration
Actuel :
config.ini Ă la racine.
Amélioration :
Utiliser un fichier PHP retournant un tableau, plus flexible :
// config.php
return [
'siteName' => 'Mon Site',
'defaultPage' => 'accueil',
'cacheEnabled' => true,
'cacheLifetime' => 86400,
// ...
];
4ïžâŁ Gestion des erreurs et logs
đ Journalisation (logging)
Actuel :
Les erreurs sâaffichent Ă lâĂ©cran (en mode debug).
Professionnel :
Rediriger toutes les erreurs vers un fichier de log en production.
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/logs/error.log');
đ« Page 404 personnalisĂ©e
Actuel :
die('Page introuvable.') en clair.
Amélioration :
Afficher une belle page 404 (avec le menu, le thĂšme, etc.) et envoyer le bon code HTTP.
http_response_code(404);
$page = '404'; // ou un fichier MD dédié
$file = $pagesDir . '/404.md';
// ... continuer le traitement normal (mais avec un contenu spécifique)
5ïžâŁ ScalabilitĂ© â PrĂ©parer lâavenir
đ§Ș SystĂšme de routage avancĂ©
Actuel :
?page=... dans lâURL.
Ăvolution :
Utiliser le routage propre (URL rewriting) :
example.com/accueilâ redirige vers?page=accueil- Ajouter un fichier
.htaccesspour réécrire les URLs.
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?page=$1 [QSA,L]
đ Mise en cache distribuĂ© (Memcached / Redis)
Pour un site Ă fort trafic, remplacer le cache fichier par Redis ou Memcached.
Cela permet de partager le cache entre plusieurs serveurs.
Exemple dâadaptation :
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$cacheKey = 'page_' . md5($page);
$html = $redis->get($cacheKey);
if ($html === false) {
// générer et stocker
$redis->set($cacheKey, $html, 86400);
}
đïž Gestion des sessions (si authentification)
Actuel :
Session PHP avec cookies.
Sécurisation :
- Utiliser
session_regenerate_id(true)aprÚs login. - Stocker les sessions en base de données (ou Redis) pour les environnements multi-serveurs.
6ïžâŁ DĂ©ploiement et maintenance
đ Environnements multiples
Créer trois environnements :
- Dev (local) : affichage des erreurs, cache désactivé.
- Staging (préproduction) : similaire à la production, mais avec logs détaillés.
- Production : erreurs masquĂ©es, cache activĂ©, logs dâerreur.
Solution :
Utiliser une variable dâenvironnement (APP_ENV) et adapter la configuration en consĂ©quence.
$env = getenv('APP_ENV') ?: 'dev';
$config = require __DIR__ . "/config/config.$env.php";
đ§Ș Tests unitaires
Ajouter des tests avec PHPUnit pour valider :
- La génération du menu.
- La gestion du cache (purge, régénération).
- La sécurité des chemins.
Exemple de test :
use PHPUnit\Framework\TestCase;
class PageManagerTest extends TestCase {
public function testLoadValidPage() {
$pm = new PageManager(__DIR__ . '/fixtures/pages', __DIR__ . '/fixtures/cache', 'accueil');
$this->assertNotNull($pm->loadPage('accueil'));
}
}
đŠ DĂ©ploiement automatisĂ©
Utiliser un outil comme Deployer, Capistrano ou un simple script bash pour :
- Extraire le code depuis Git.
- Installer les dépendances (Composer).
- Vider le cache (si nécessaire).
- Redémarrer le serveur web.
7ïžâŁ Checklist finale â Les 10 actions prioritaires
| # | Action | Impact | Difficulté |
|---|---|---|---|
| 1 | Sécuriser les chemins avec realpath() |
đ„ Critique | Facile |
| 2 | Remplacer extract() par accĂšs direct |
đ„ Critique | Facile |
| 3 | Mettre en place le logging dâerreurs | ĂlevĂ© | Facile |
| 4 | Ajouter des en-tĂȘtes de cache HTTP | ĂlevĂ© | Facile |
| 5 | Protéger .cache/ par .htaccess |
ĂlevĂ© | Facile |
| 6 | Créer une page 404 personnalisée | Moyen | Facile |
| 7 | Passer à Composer pour les dépendances | Moyen | Moyen |
| 8 | Mettre en cache le menu HTML | Moyen | Facile |
| 9 | Ajouter un cron pour nettoyer le cache | Moyen | Facile |
| 10 | Utiliser des variables dâenvironnement pour la config | Moyen | Moyen |
đ Ressources utiles
- PHP The Right Way â Bonnes pratiques PHP.
- Mozilla Web Security â SĂ©curitĂ© web.
- Composer â Gestionnaire de dĂ©pendances.
- Redis / Memcached â Caches distribuĂ©s.
âïž Ă conserver prĂ©cieusement â Applique ces amĂ©liorations progressivement, par prioritĂ©. Ton serveur deviendra une vĂ©ritable application professionnelle, fiable et performante.