207 lines
6.8 KiB
Markdown
207 lines
6.8 KiB
Markdown
# 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.
|