feat: support du déploiement sous sous-chemin (paths.base)

- Liens et assets via {base}, polices déplacées vers src/lib/assets (hachage Vite)
- Service worker base-aware, précache sur les pages prérendues
- CSP hash documentée : l'hôte ne doit pas fixer script-src
- Docs : déploiement Apache/YunoHost (DEPLOYMENT, APACHE_AUTOINDEX)
This commit is contained in:
sucupira
2026-07-21 13:41:10 -04:00
parent 45fd4bd407
commit 3e60f48c5e
18 changed files with 206 additions and 42 deletions
+66
View File
@@ -0,0 +1,66 @@
# Piège Apache (o2switch) : « Index of /… » au lieu des pages
Symptôme : le site est déployé sur un hébergement mutualisé Apache (o2switch),
la page d'accueil s'affiche, mais **cliquer sur un lien interne affiche
« Index of /chatcontrol/quiz »** (un listing de dossier) au lieu de la page.
## Cause : conflit fichier / dossier
SvelteKit (adapter-static, `trailingSlash` par défaut = `'never'`) génère :
```
build/
├── quiz.html ← la page
├── quiz/ ← dossier créé pour __data.json
│ └── __data.json
├── outils.html
├── outils/
│ └── …
```
Quand le navigateur demande `/chatcontrol/quiz`, Apache voit qu'un **dossier**
`quiz/` existe et redirige (301) vers `/chatcontrol/quiz/`. Comme ce dossier
ne contient pas de `index.html`, Apache sert son **autoindex** (le listing).
Le fichier `quiz.html`, lui, n'est jamais trouvé.
Apache donne toujours la priorité au dossier quand les deux existent —
ce problème revient à chaque site statique posé sur du mutualisé Apache.
## Correctif (déjà appliqué dans ce dépôt)
`frontend/src/routes/+layout.ts` :
```ts
export const trailingSlash = 'always'
```
Chaque page est alors générée **dans** son dossier :
```
build/
├── quiz/
│ ├── index.html ← la page, servie par Apache
│ └── __data.json
├── outils/
│ ├── index.html
│ └── aegis/
│ └── index.html
```
Plus de conflit possible : le dossier existe, il contient un `index.html`,
Apache le sert.
## Checklist quand ça arrive
1. `export const trailingSlash = 'always'` dans le `+layout.ts` racine.
2. `pnpm build` → vérifier que `build/quiz/index.html` existe
(et non `build/quiz.html`).
3. **Vider le dossier distant avant de re-uploader** : les anciens
`quiz.html` / `outils.html` et les vieux assets `_app/immutable/` hashés
doivent partir, sinon ils traînent indéfiniment.
4. Tester `curl -s https://<site>/<page>/` → du HTML, pas « Index of ».
Note : les liens internes relatifs (`./outils`) provoquent une petite
redirection 301 vers l'URL avec `/` final. Normal, invisible pour
l'utilisateur. L'éviter exigerait `paths.relative: false`, au prix de la
consultation locale des fichiers du build.
+71 -17
View File
@@ -21,6 +21,28 @@ pnpm install
pnpm build # produit frontend/build/ (~60 pages statiques)
```
> **Racine de domaine** : le site est servi à la racine de
> `https://chatcontrol.o-k-i.net/` (YunoHost, voir §5) — aucun `paths.base`.
> Pour un déploiement sous un sous-chemin, ajoute `paths: { base: '/prefixe' }`
> dans `frontend/svelte.config.js` avant `pnpm build` et adapte les règles
> `try_files` côté serveur (ex. nginx : `location /prefixe/ { … }`).
> **Mutualisé Apache (o2switch)** : si les liens internes affichent
> « Index of /… » (listing de dossier) au lieu des pages, c'est le conflit
> fichier/dossier classique — voir [APACHE_AUTOINDEX.md](./APACHE_AUTOINDEX.md).
> **CSP et hydratation (critique)** : SvelteKit démarre côté navigateur via
> un script *inline* (qui embarque les données de la page). Un en-tête HTTP
> `Content-Security-Policy` contenant `script-src 'self'` — comme celui posé
> par le compte o2switch — **bloque ce script et tue toute l'interactivité**
> (recherche, filtres, sélecteur de langue), sans message visible. Chaque page
> prérendue porte donc sa propre `<meta>` CSP avec l'empreinte (`sha256-…`) de
> son script inline (`csp.mode: 'hash'` dans `frontend/svelte.config.js`).
> Règle côté serveur : **ne jamais fixer `script-src` dans l'en-tête HTTP** —
> le retirer de la config cPanel/`.htaccess`/reverse proxy (le reste de la CSP
> peut y rester). Si l'hébergeur impose un `script-src` en en-tête, le régler
> sur `script-src 'self' 'unsafe-inline'` (plus faible, mais fonctionnel).
## 2. Option A — Caddy (recommandé : TLS automatique)
```sh
@@ -32,7 +54,7 @@ sudo mkdir -p /srv/lage-chat-control
`/etc/caddy/Caddyfile` :
```caddy
exemple.o-k-i.net {
chatcontrol.o-k-i.net {
root * /srv/lage-chat-control
file_server
encode zstd gzip
@@ -50,8 +72,11 @@ exemple.o-k-i.net {
X-Content-Type-Options "nosniff"
Referrer-Policy "no-referrer"
Permissions-Policy "camera=(), microphone=(), geolocation=()"
# CSP stricte : le site n'a aucune dépendance externe
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'"
# CSP stricte : le site n'a aucune dépendance externe.
# NE PAS remettre script-src ici : il est porté par la <meta> CSP de
# chaque page (empreinte du script inline de démarrage, différente
# d'une page à l'autre — inexprimable dans un en-tête statique).
Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'"
}
}
```
@@ -68,13 +93,13 @@ sudo apt install nginx certbot python3-certbot-nginx
```nginx
server {
server_name exemple.o-k-i.net;
server_name chatcontrol.o-k-i.net;
root /srv/lage-chat-control;
index index.html;
# pages pré-générées : /outils → outils.html, fallback 404
# pages pré-générées : /outils/ → outils/index.html, fallback 404
location / {
try_files $uri $uri.html $uri/index.html /404.html;
try_files $uri $uri/ $uri.html $uri/index.html /404.html;
}
location /_app/immutable/ {
@@ -88,7 +113,8 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'" always;
add_header Content-Security-Policy "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'" always;
# NB : pas de script-src ici — voir le commentaire CSP de l'option A.
gzip on;
gzip_types text/html text/css application/javascript application/json image/svg+xml;
@@ -98,7 +124,7 @@ server {
```sh
sudo ln -s /etc/nginx/sites-available/lage-chat-control /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d exemple.o-k-i.net # TLS
sudo certbot --nginx -d chatcontrol.o-k-i.net # TLS
```
## 4. Publier (et republier)
@@ -112,16 +138,37 @@ C'est l'intégralité du « pipeline de déploiement ». Pour automatiser :
un runner Forgejo Actions sur labola peut exécuter build + rsync à chaque
push sur `main` (à ajouter quand le besoin se présente, pas avant).
## 5. Option C — YunoHost
## 5. Option C — YunoHost (cible de production)
Le site s'installe avec l'app **`my_webapp`** (site statique) :
Le site tourne en production sur **`chatcontrol.o-k-i.net`** via l'app
**`my_webapp`** (site statique) :
1. Installe `my_webapp` depuis le catalogue, choisis le domaine/chemin.
2. Copie le contenu de `frontend/build/` dans le dossier `www/` de l'app
(SFTP ou rsync avec l'utilisateur dédié créé par l'app).
3. Dans la config nginx avancée de l'app, ajoute le bloc `try_files` ci-dessus.
1. Installe `my_webapp` depuis le catalogue YunoHost avec le domaine
`chatcontrol.o-k-i.net`, le chemin `/`, un accès **public** (visiteurs
compris), sans PHP ni base de données.
2. Copie le contenu de `frontend/build/` dans le `www/` de l'app, avec
l'utilisateur SFTP dédié créé par l'app (son nom exact figure dans la
page de l'app, section « Accès SFTP ») :
```sh
rsync -avz --delete frontend/build/ my_webapp@chatcontrol.o-k-i.net:www/
```
3. C'est tout : la conf nginx générée sert déjà `<route>/index.html` (avec
redirection automatique vers le `/` final). YunoHost gère TLS, HSTS et
le renouvellement des certificats.
YunoHost gère TLS, HSTS et le renouvellement des certificats.
Durcissements optionnels (panneau de config de l'app, ou
`/etc/nginx/conf.d/chatcontrol.o-k-i.net.d/my_webapp.conf`) :
- `error_page 404 /404.html;` — notre page 404 plutôt que celle de nginx ;
- cache long sur les assets immuables :
`location /_app/immutable/ { add_header Cache-Control "public, max-age=31536000, immutable"; }`
- revalidation du service worker :
`location = /service-worker.js { add_header Cache-Control "no-cache"; }`
Ne pose **pas** d'en-tête CSP globale côté YunoHost : la `<meta>` CSP de
chaque page suffit (voir l'encadré CSP en §1). À noter : l'overlay du portail
YunoHost, injecté uniquement pour les visiteurs déjà connectés au SSO, est
bloqué par cette CSP — cosmétique, sans effet pour les visiteurs anonymes.
## 6. Option D — Docker
@@ -144,8 +191,15 @@ majorité des cas.
## 7. Vérifier après mise en ligne
- [ ] `curl -sI https://exemple.o-k-i.net | grep -i content-security` → CSP présente
- [ ] La page `/outils` répond (et `/outils?cat=messagerie` filtre)
- [ ] `curl -s https://chatcontrol.o-k-i.net/outils/ | grep -o 'http-equiv="Content-Security-Policy"'`
→ la `<meta>` CSP par page est présente (elle porte `script-src` + empreinte)
- [ ] `curl -sI https://chatcontrol.o-k-i.net/outils/ | grep -i content-security`
→ l'en-tête HTTP éventuelle ne contient **pas** de `script-src` bloquant
- [ ] Ouvrir `/outils`, taper « signal » dans la recherche → la liste
se filtre sans rechargement (preuve que l'hydratation fonctionne) ;
cliquer une catégorie → l'URL gagne `?cat=…` et la liste se filtre
- [ ] DevTools > Console : aucune erreur CSP (« violates the following Content
Security Policy directive ») au chargement
- [ ] DevTools > Application > Service worker : enregistré ; passer hors-ligne
et recharger une fiche déjà visitée → elle s'affiche
- [ ] observatory.mozilla.org : note A/A+ attendue avec les en-têtes ci-dessus