# F3 Simple Blog Blog simple avec Fat-Free Framework, SQLite et une petite médiathèque d’images. ## Structure ```text project/ ├── config.local.ini # Surcharges locales (gitignored) ├── app/ │ ├── config.ini # Configuration F3 (globals + routes) │ ├── bootstrap.php # Initialisation (config, DB, session, erreurs) │ ├── Controllers/ │ ├── Helpers/ # Fonctions utilitaires │ ├── Models/ # DB\SQL\Mapper (Post, Media, User) │ ├── Services/ # MarkdownService │ └── Views/ ├── db/ │ └── app.sqlite # Base SQLite persistante ├── logs/ │ └── php-error.log # Log PHP configuré au runtime ├── public/ │ ├── assets/ # Sources CSS/JS servies via /min/@file │ └── uploads/ │ └── media/ # Images publiées (JPG conservé, PNG/WebP normalisés en PNG) ├── scripts/ │ ├── install.php # Initialisation idempotente de la base │ └── create-admin.php # Création d’un compte admin en CLI └── tmp/ ├── cache/ # Cache F3 + assets minifiés └── uploads/ # Transit Web::receive(), nettoyé après chaque upload ``` ## Philosophie des dossiers runtime Le projet sépare les données persistantes du runtime jetable : - `tmp/` = runtime temporaire, recréable - `db/` = base SQLite persistante - `logs/` = logs persistants - `public/uploads/media/` = médias publiés et persistants Autrement dit, `tmp/` peut être vidé sans perte métier. Les données à sauvegarder restent hors de `tmp/`. ## Fonctionnalités F3 utilisées - **Routage nommé** — `config.ini [routes]`, filtre `alias` dans les templates, `reroute('@route')` dans les contrôleurs - **Cache HTTP / F3** — TTL appliqués dans les contrôleurs avec `expire()` - accueil : `300 s` - page article : `3600 s` - assets minifiés : `86400 s` - **Assets minifiés** — `Web::minify()` via `AssetController` (`GET /min/@file`) - **Upload** — `Web::receive()` avec contrôle de taille, puis validation MIME/dimensions côté modèle - **Images** — normalisation des médias via GD (`JPG` conservé, `PNG/WebP` convertis en `PNG` pour préserver la transparence) - **Markdown** — `Markdown::instance()->convert()` + reconstruction DOM en liste blanche - **Slugs** — `Web::instance()->slug()` - **Session / CSRF** — `$f3->set('JAR', …)`, hooks `beforeRoute()` sur les contrôleurs protégés, jeton exposé via `@CSRF` puis recopié en session au rendu pour vérification lors du POST suivant - **ORM** — `DB\SQL\Mapper` : `paginate()`, `copyfrom()`, `cast()`, `find()` - **Erreurs** — gestion personnalisée en production via `ONERROR` + fallback HTML minimal sur erreur fatale ## Prérequis ### Développement local - PHP 8.3+ - Composer - Extensions PHP : `pdo_sqlite`, `dom`, `gd`, `mbstring`, `intl` ### Déploiement Docker - Docker - Docker Compose ## Configuration Les paramètres par défaut sont dans `app/config.ini`. Pour surcharger localement ou en production : ```bash cp config.local.ini.example config.local.ini ``` Réglages minimums conseillés en production : ```ini [globals] app.env=prod app.timezone=Europe/Paris ``` Le fichier `config.local.ini` sert uniquement aux surcharges d’environnement. Les chemins runtime restent les mêmes partout : - `tmp/cache/` pour le cache F3 et les assets minifiés - `tmp/uploads/` pour les fichiers temporaires d’upload Les données persistantes restent hors de `tmp` : `db/`, `logs/`, `public/uploads/media/`. ## Développement local ```bash composer install cp config.local.ini.example config.local.ini php scripts/install.php php -S 127.0.0.1:8080 -t public ``` Ouvre ensuite `http://127.0.0.1:8080`. Créer un compte admin : ```bash php scripts/create-admin.php admin # mot de passe : 10 caractères minimum ``` ## Déploiement avec Docker ```bash cp config.local.ini.example config.local.ini # édite config.local.ini (app.env=prod, app.timezone, etc.) docker compose up -d --build ``` Docker ne monte que les dossiers persistants (`db/`, `logs/`, `public/uploads/media/`) et laisse `tmp/` dans le conteneur pour qu’il reste réellement éphémère. Le fichier `config.local.ini` est monté en lecture seule. Si le fichier hôte n’existe pas, Docker peut créer un répertoire à la place ; l’entrypoint le supprime et l’application retombe alors sur les valeurs par défaut de `app/config.ini`. Le service écoute sur `http://127.0.0.1:8888`. Créer un compte admin : ```bash docker compose exec app php scripts/create-admin.php admin # mot de passe : 10 caractères minimum ``` ## Cache public et navigation Les pages publiques restent cacheables pour un visiteur anonyme : - `/` est servie avec un TTL de `300 s` - `/posts/@slug` est servie avec un TTL de `3600 s` - `/min/app.css` et `/min/app.js` sont servis avec un TTL de `86400 s` Quand un utilisateur est connecté, le layout dépend de la session (navigation admin + formulaire de déconnexion avec CSRF). Le rendu est alors forcé en non-cacheable avec `expire(0)`. Le projet ne fait pas d’invalidation explicite du cache public lors des mutations d’articles : la fraîcheur dépend donc des TTL ci-dessus. ## Médias et limites d’upload - Formats acceptés à l’entrée : `JPG`, `PNG`, `WebP` - Taille max du fichier reçu : `10 Mo` - Dimensions max : `8000 × 8000 px` - Limite de surface : `40 mégapixels` - Sortie publiée : `JPG` pour les sources JPEG, `PNG` pour les sources PNG/WebP - Texte alternatif initial dérivé du nom de fichier d’origine La médiathèque admin est paginée et le picker dans l’éditeur charge seulement les `60` images les plus récentes pour éviter de charger toute la bibliothèque en mémoire à chaque formulaire. ## Reverse proxy Caddy ### Caddy sur le même hôte ```caddy blog.example.com { encode zstd gzip reverse_proxy 127.0.0.1:8888 } ``` ### Caddy dans Docker Si Caddy tourne aussi dans Docker, place-le sur le même réseau que `app` et cible directement le service : ```caddy blog.example.com { encode zstd gzip reverse_proxy app:80 } ``` Le fichier `Caddyfile.example` fournit en plus un jeu d’en-têtes de sécurité minimal. ## Données à sauvegarder - `db/` — base SQLite - `public/uploads/media/` — images - `logs/` — optionnel, utile pour diagnostic - `tmp/` — non persistant, recréable ## Mise à jour ```bash docker compose up -d --build ``` ## Logs - PHP : `logs/php-error.log` - Apache / conteneur : `docker compose logs -f app` ## Notes - Les dates sont stockées en UTC (`gmdate`) puis formatées côté affichage avec le fuseau configuré. - `scripts/install.php` peut être relancé sans danger : il crée les tables si elles n’existent pas.