218 lines
7.7 KiB
Markdown
218 lines
7.7 KiB
Markdown
# F3 Simple Blog
|
||
|
||
Blog simple avec Fat-Free Framework et SQLite.
|
||
|
||
## 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 ciblées
|
||
│ ├── 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/ # Assets statiques servis directement
|
||
│ └── uploads/
|
||
│ └── media/ # Images publiées (JPG conservé, PNG/WebP normalisés en PNG)
|
||
├── scripts/
|
||
│ ├── bootstrap.php # Autoload + bootstrap partagé par les scripts CLI
|
||
│ ├── install.php # Initialisation idempotente de la base
|
||
│ └── create-admin.php # Création d'un compte admin en CLI
|
||
└── tmp/ # Runtime temporaire, recréable sans perte métier
|
||
└── uploads/ # Transit Web::receive(), nettoyé après chaque upload
|
||
```
|
||
|
||
## Fonctionnalités F3 utilisées
|
||
|
||
- **Routage nommé** — `config.ini [routes]`, filtre `alias` dans les templates, `reroute('@route')` dans les contrôleurs.
|
||
- **Cache HTTP** — TTL par contrôleur via `expire()`, forcé à `0` quand un utilisateur est connecté.
|
||
- **Assets statiques** — servis directement depuis `public/assets/`, laissés au serveur HTTP / reverse proxy.
|
||
- **Upload** — `Web::receive()` avec contrôle de taille, puis validation MIME/dimensions côté modèle.
|
||
- **Images** — normalisation des médias via `Image`, lecture/écriture via `Base::read()` / `Base::write()`.
|
||
- **Markdown** — `Markdown::instance()->convert()` suivi d'un assainissement DOM ciblé et de la résolution des images média.
|
||
- **Slugs** — `Web::instance()->slug()`.
|
||
- **Session / CSRF** — `$f3->set('JAR', …)`, `new Session(null, 'CSRF')`, hooks `beforeRoute()` sur les contrôleurs protégés, et petit pool de jetons côté session pour éviter les collisions multi-onglets.
|
||
- **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`, `gd`, `mbstring`, `intl`, `dom`
|
||
|
||
### 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
|
||
```
|
||
|
||
### Proxies de confiance
|
||
|
||
L'application ne délègue pas la sécurité des cookies à Apache : elle détermine le schéma de la requête pour marquer la session en `Secure`.
|
||
|
||
La valeur par défaut couvre les cas les plus courants :
|
||
|
||
- `127.0.0.1,::1` — Caddy sur le même hôte ;
|
||
- `172.16.0.0/12` — réseaux Docker IPv4 standard, y compris un Caddy conteneurisé sur le même réseau.
|
||
|
||
Si ton proxy tourne sur un autre sous-réseau, surcharge `app.trusted_proxies` dans `config.local.ini`.
|
||
|
||
```ini
|
||
[globals]
|
||
app.trusted_proxies=127.0.0.1,::1,172.16.0.0/12
|
||
# ou, avec une liste F3 : app.trusted_proxies[]=10.0.0.42
|
||
```
|
||
|
||
Seuls ces proxies sont autorisés à influencer `X-Forwarded-Proto` / `Forwarded`.
|
||
|
||
## 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
|
||
```
|
||
|
||
`scripts/install.php` peut être relancé sans danger : il crée les tables si elles n'existent pas.
|
||
|
||
## Déploiement avec Docker
|
||
|
||
```bash
|
||
cp config.local.ini.example config.local.ini
|
||
# édite config.local.ini (app.env=prod, app.timezone, trusted proxies si besoin)
|
||
docker compose up -d --build
|
||
```
|
||
|
||
Le déploiement Docker est cohérent avec le projet :
|
||
|
||
- l'image embarque les extensions réellement utilisées (`pdo_sqlite`, `gd`, `mbstring`, `intl`, `dom`) ;
|
||
- `scripts/install.php` s'exécute au démarrage pour initialiser SQLite de façon idempotente ;
|
||
- seuls les répertoires métier/persistants sont montés (`db/`, `logs/`, `public/uploads/media/`) ;
|
||
- `tmp/` reste dans le conteneur et sert uniquement au runtime F3 (transit d'upload, fichiers temporaires).
|
||
|
||
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 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
|
||
|
||
Les pages publiques restent cacheables pour un visiteur anonyme :
|
||
|
||
- `/` : TTL de 300 s ;
|
||
- `/posts/@slug` : TTL de 3600 s.
|
||
|
||
Quand un utilisateur est connecté, le rendu est forcé en non-cacheable avec `expire(0)` pour ne pas servir du contenu admin via un cache intermédiaire.
|
||
|
||
Les assets statiques (`public/assets/`) sont servis directement ; leur éventuel cache HTTP relève du serveur web ou du reverse proxy.
|
||
|
||
## 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, 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.
|
||
|
||
## 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
|
||
}
|
||
```
|
||
|
||
Dans la plupart des cas, la valeur par défaut de `app.trusted_proxies` suffit aussi pour ce mode ; surcharge-la seulement si ton réseau Docker utilise un autre sous-réseau.
|
||
|
||
Comme F3 lit aussi `X-Forwarded-For` pour déterminer `IP`, le conteneur applicatif ne doit pas être exposé directement à Internet : Caddy doit rester l'unique point d'entrée public et réécrire les en-têtes `X-Forwarded-*`.
|
||
|
||
Le fichier `Caddyfile.example` fournit en plus un jeu d'en-têtes de sécurité minimal.
|
||
|
||
## Répartition des responsabilités avec Caddy
|
||
|
||
Le projet n'essaie pas de dupliquer ce que le reverse proxy gère naturellement :
|
||
|
||
- **Caddy** — TLS, compression, en-têtes de sécurité, terminaison HTTPS, reverse proxy.
|
||
- **Application / PHP-F3** — sessions, CSRF, cookies, validation métier, rendu HTML et cache HTTP des pages dynamiques.
|
||
|
||
## Données à sauvegarder
|
||
|
||
- `db/` — base SQLite.
|
||
- `public/uploads/media/` — images.
|
||
- `logs/` — optionnel, utile pour diagnostic.
|
||
|
||
## 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é.
|
||
- Le pipeline Markdown n'autorise que les éléments utiles au rendu éditorial et remappe systématiquement les images vers la médiathèque locale.
|