From 3e60f48c5e7962428748754de4c8e5d2e93cf725 Mon Sep 17 00:00:00 2001 From: sucupira Date: Tue, 21 Jul 2026 13:41:10 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20support=20du=20d=C3=A9ploiement=20sous?= =?UTF-8?q?=20sous-chemin=20(paths.base)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .gitignore | 3 + docs/APACHE_AUTOINDEX.md | 66 +++++++++++++ docs/DEPLOYMENT.md | 88 ++++++++++++++---- ...kinson-hyperlegible-latin-400-normal.woff2 | Bin .../fonts/fraunces-latin-700-normal.woff2 | Bin .../src/lib/components/CategoryNav.svelte | 3 +- .../src/lib/components/ShortcutsDialog.svelte | 5 +- frontend/src/lib/components/ToolCard.svelte | 3 +- frontend/src/routes/+layout.svelte | 9 +- frontend/src/routes/+layout.ts | 6 ++ frontend/src/routes/+page.svelte | 5 +- frontend/src/routes/outils/+page.svelte | 3 +- .../src/routes/outils/[slug]/+page.svelte | 5 +- frontend/src/routes/quiz/+page.svelte | 7 +- frontend/src/service-worker.js | 15 ++- frontend/src/styles/fonts.css | 6 +- frontend/static/site.webmanifest | 4 +- frontend/svelte.config.js | 20 ++++ 18 files changed, 206 insertions(+), 42 deletions(-) create mode 100644 docs/APACHE_AUTOINDEX.md rename frontend/{static => src/lib/assets}/fonts/atkinson-hyperlegible-latin-400-normal.woff2 (100%) rename frontend/{static => src/lib/assets}/fonts/fraunces-latin-700-normal.woff2 (100%) diff --git a/.gitignore b/.gitignore index 15dffdd..8bdb1a6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ node_modules/ # SvelteKit (frontend/) frontend/.svelte-kit/ frontend/build/ + +# Brief de mission (document de travail local) +testoki_exit_chat_control.md diff --git a/docs/APACHE_AUTOINDEX.md b/docs/APACHE_AUTOINDEX.md new file mode 100644 index 0000000..cc9a6c6 --- /dev/null +++ b/docs/APACHE_AUTOINDEX.md @@ -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:////` → 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. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 9cbd740..29cfcd3 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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 `` 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 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à `/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 `` 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 `` 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 diff --git a/frontend/static/fonts/atkinson-hyperlegible-latin-400-normal.woff2 b/frontend/src/lib/assets/fonts/atkinson-hyperlegible-latin-400-normal.woff2 similarity index 100% rename from frontend/static/fonts/atkinson-hyperlegible-latin-400-normal.woff2 rename to frontend/src/lib/assets/fonts/atkinson-hyperlegible-latin-400-normal.woff2 diff --git a/frontend/static/fonts/fraunces-latin-700-normal.woff2 b/frontend/src/lib/assets/fonts/fraunces-latin-700-normal.woff2 similarity index 100% rename from frontend/static/fonts/fraunces-latin-700-normal.woff2 rename to frontend/src/lib/assets/fonts/fraunces-latin-700-normal.woff2 diff --git a/frontend/src/lib/components/CategoryNav.svelte b/frontend/src/lib/components/CategoryNav.svelte index 756d7c1..3fd12e1 100644 --- a/frontend/src/lib/components/CategoryNav.svelte +++ b/frontend/src/lib/components/CategoryNav.svelte @@ -1,5 +1,6 @@ diff --git a/frontend/src/lib/components/ShortcutsDialog.svelte b/frontend/src/lib/components/ShortcutsDialog.svelte index e681514..fb64ebd 100644 --- a/frontend/src/lib/components/ShortcutsDialog.svelte +++ b/frontend/src/lib/components/ShortcutsDialog.svelte @@ -1,5 +1,6 @@ diff --git a/frontend/src/lib/components/ToolCard.svelte b/frontend/src/lib/components/ToolCard.svelte index ed6e1d6..d36451a 100644 --- a/frontend/src/lib/components/ToolCard.svelte +++ b/frontend/src/lib/components/ToolCard.svelte @@ -1,4 +1,5 @@ diff --git a/frontend/src/routes/outils/[slug]/+page.svelte b/frontend/src/routes/outils/[slug]/+page.svelte index 52ca290..89293a3 100644 --- a/frontend/src/routes/outils/[slug]/+page.svelte +++ b/frontend/src/routes/outils/[slug]/+page.svelte @@ -1,4 +1,5 @@