📁 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

  1. Scanne récursivement tout le dossier pages/.
  2. Calcule le hash actuel du dossier en prenant en compte :
    • Le chemin de chaque fichier.
    • Sa taille.
    • Sa date de derniĂšre modification.
  3. Compare ce hash avec l'ancien hash stocké dans listFiles.hash.
  4. Si le hash a changé (ou si listFiles.html n'existe pas) :
    • 🔄 On reconstruit entiĂšrement l'arborescence HTML.
    • 📝 On Ă©crase listFiles.html avec 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

  1. Vérifie l'existence du flag .enableCache :

    • ❌ Si le flag est ABSENT :
      • On supprime TOUS les fichiers .html du dossier .cache/.
      • On recrĂ©e immĂ©diatement le flag .enableCache (pour Ă©viter que le prochain visiteur ne supprime Ă  nouveau).
    • ✅ Si le flag est PRÉSENT : On ne touche pas au dossier .cache/.
  2. RécupÚre le nom de la page via $_GET['page'] (ex: accueil).

  3. Sécurise le nom (basename() pour éviter les attaques type ../).

  4. Définit les chemins :

    • Source : pages/accueil.md
    • Cache : .cache/md5(accueil).html
  5. Vérifie si le cache individuel est valide :

    • Si le fichier .html n'existe pas → on le gĂ©nĂšre.
    • Si le fichier .md est plus rĂ©cent que le .html → on le rĂ©gĂ©nĂšre.
  6. 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

  1. Admin crée pages/nouveau.md (attention à l'extension .md en minuscules !).
  2. L'admin rafraĂźchit le menu (ou appelle pages/index.php).
  3. pages/index.php calcule le hash → diffĂ©rent de l'ancien.
  4. Il régénÚre listFiles.html et supprime .enableCache.
  5. Premier visiteur arrive sur le site.
  6. index.php voit que .enableCache est absent → vide tout .cache/ → recrĂ©e .enableCache.
  7. Le visiteur demande ?page=nouveau → le fichier .html n'existe pas, il est gĂ©nĂ©rĂ© et affichĂ©.
  8. 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)

  1. Admin modifie pages/accueil.md.
  2. L'admin rafraĂźchit le menu.
  3. pages/index.php calcule le hash → change (car la date de modification a changĂ©).
  4. Il régénÚre listFiles.html et supprime .enableCache.
  5. Premier visiteur arrive → le flag est absent → vide tout .cache/ (mĂȘme les pages qui n'ont pas changĂ©).
  6. Le visiteur demande accueil → comme le cache est vidĂ©, il est rĂ©gĂ©nĂ©rĂ© avec le nouveau contenu.
  7. Les autres pages sont régénérées au fur et à mesure des demandes.

Scénario C : Aucune modification (cas nominal)

  1. Le flag .enableCache est présent.
  2. Le hash dans pages/listFiles.hash correspond au dossier actuel.
  3. index.php lit directement le fichier .cache/accueil.html.
  4. 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

  1. Ne jamais recréer .enableCache ailleurs que dans index.php (sinon le systÚme de vidange ne se déclenchera pas).
  2. Le hash est roi : si jamais listFiles.html est obsolÚte, c'est toujours le hash qui décidera de le regénérer.
  3. 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 .md devient plus rĂ©cente que celle du .html).
  4. Pense à inclure Parsedown : télécharge-le ou installe-le via Composer (composer require erusev/parsedown) et ajuste le require_once dans index.php.
  5. Les nouveaux fichiers doivent avoir l'extension .md (et non .HTML, .html, .txt, etc.) pour ĂȘtre reconnus par buildTree.
  6. 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 .cache ou pages/.


📋 Pour copier ce texte facilement

Si la copie ne fonctionne pas, essaye ces astuces :

  1. Sélectionne tout avec Ctrl+A (ou Cmd+A sur Mac) puis Ctrl+C (ou Cmd+C).
  2. Utilise le bouton de copie en haut Ă  droite du bloc de code (si ton interface l'affiche).
  3. 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

  1. Scanne récursivement tout le dossier pages/.
  2. Calcule le hash actuel du dossier en prenant en compte :
    • Le chemin de chaque fichier.
    • Sa taille.
    • Sa date de derniĂšre modification.
  3. Compare ce hash avec l'ancien hash stocké dans listFiles.hash.
  4. Si le hash a changé (ou si listFiles.html n'existe pas) :
    • 🔄 On reconstruit entiĂšrement l'arborescence HTML.
    • 📝 On Ă©crase listFiles.html avec 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

  1. Vérifie l'existence du flag .enableCache :

    • ❌ Si le flag est ABSENT :
      • On supprime TOUS les fichiers .html du dossier .cache/.
      • On recrĂ©e immĂ©diatement le flag .enableCache (pour Ă©viter que le prochain visiteur ne supprime Ă  nouveau).
    • ✅ Si le flag est PRÉSENT : On ne touche pas au dossier .cache/.
  2. RécupÚre le nom de la page via $_GET['page'] (ex: accueil).

  3. Sécurise le nom (basename() pour éviter les attaques type ../).

  4. Définit les chemins :

    • Source : pages/accueil.md
    • Cache : .cache/md5(accueil).html
  5. Vérifie si le cache individuel est valide :

    • Si le fichier .html n'existe pas → on le gĂ©nĂšre.
    • Si le fichier .md est plus rĂ©cent que le .html → on le rĂ©gĂ©nĂšre.
  6. 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

  1. Admin crée pages/nouveau.md.
  2. L'admin rafraĂźchit le menu (ou appelle pages/index.php).
  3. pages/index.php calcule le hash → diffĂ©rent de l'ancien.
  4. Il régénÚre listFiles.html et supprime .enableCache.
  5. Premier visiteur arrive sur le site.
  6. index.php voit que .enableCache est absent → vide tout .cache/ → recrĂ©e .enableCache.
  7. Le visiteur demande ?page=nouveau → le fichier .html n'existe pas, il est gĂ©nĂ©rĂ© et affichĂ©.
  8. 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)

  1. Admin modifie pages/accueil.md.
  2. L'admin rafraĂźchit le menu.
  3. pages/index.php calcule le hash → change (car la date de modification a changĂ©).
  4. Il régénÚre listFiles.html et supprime .enableCache.
  5. Premier visiteur arrive → le flag est absent → vide tout .cache/ (mĂȘme les pages qui n'ont pas changĂ©).
  6. Le visiteur demande accueil → comme le cache est vidĂ©, il est rĂ©gĂ©nĂ©rĂ© avec le nouveau contenu.
  7. Les autres pages sont régénérées au fur et à mesure des demandes.

Scénario C : Aucune modification (cas nominal)

  1. Le flag .enableCache est présent.
  2. Le hash dans pages/listFiles.hash correspond au dossier actuel.
  3. index.php lit directement le fichier .cache/accueil.html.
  4. 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

  1. Ne jamais recréer .enableCache ailleurs que dans index.php (sinon le systÚme de vidange ne se déclenchera pas).
  2. Le hash est roi : si jamais listFiles.html est obsolÚte, c'est toujours le hash qui décidera de le regénérer.
  3. 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 .md devient plus rĂ©cente que celle du .html).
  4. Pense à inclure Parsedown : télécharge-le ou installe-le via Composer (composer require erusev/parsedown) et ajuste le require_once dans index.php.

✍ Fichier créé pour mĂ©moire – En cas de doute, relis cette doc avant de toucher aux dossiers .cache ou pages/.

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


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 :

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 :

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) :

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 :


6ïžâƒŁ DĂ©ploiement et maintenance

🔄 Environnements multiples

Créer trois environnements :

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 :

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 :


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


✍ À conserver prĂ©cieusement – Applique ces amĂ©liorations progressivement, par prioritĂ©. Ton serveur deviendra une vĂ©ritable application professionnelle, fiable et performante.