6.8 KiB
F3 Simple Blog
Blog simple avec Fat-Free Framework, SQLite et une petite médiathèque d’images.
Structure
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éabledb/= base SQLite persistantelogs/= logs persistantspublic/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], filtrealiasdans 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
- accueil :
- Assets minifiés —
Web::minify()viaAssetController(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 (
JPGconservé,PNG/WebPconvertis enPNGpour préserver la transparence) - Markdown —
Markdown::instance()->convert()+ reconstruction DOM en liste blanche - Slugs —
Web::instance()->slug() - Session / CSRF —
$f3->set('JAR', …), hooksbeforeRoute()sur les contrôleurs protégés, jeton exposé via@CSRFpuis 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 :
cp config.local.ini.example config.local.ini
Réglages minimums conseillés en production :
[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éstmp/uploads/pour les fichiers temporaires d’upload
Les données persistantes restent hors de tmp : db/, logs/, public/uploads/media/.
Développement local
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 :
php scripts/create-admin.php admin
# mot de passe : 10 caractères minimum
Déploiement avec Docker
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 :
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 de300 s/posts/@slugest servie avec un TTL de3600 s/min/app.csset/min/app.jssont servis avec un TTL de86400 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 :
JPGpour les sources JPEG,PNGpour 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
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 :
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 SQLitepublic/uploads/media/— imageslogs/— optionnel, utile pour diagnostictmp/— non persistant, recréable
Mise à jour
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.phppeut être relancé sans danger : il crée les tables si elles n’existent pas.