Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 848e33d198 | |||
| 3e60f48c5e |
@@ -5,3 +5,6 @@ node_modules/
|
|||||||
# SvelteKit (frontend/)
|
# SvelteKit (frontend/)
|
||||||
frontend/.svelte-kit/
|
frontend/.svelte-kit/
|
||||||
frontend/build/
|
frontend/build/
|
||||||
|
|
||||||
|
# Brief de mission (document de travail local)
|
||||||
|
testoki_exit_chat_control.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://<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
@@ -21,6 +21,28 @@ pnpm install
|
|||||||
pnpm build # produit frontend/build/ (~60 pages statiques)
|
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)
|
## 2. Option A — Caddy (recommandé : TLS automatique)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -32,7 +54,7 @@ sudo mkdir -p /srv/lage-chat-control
|
|||||||
`/etc/caddy/Caddyfile` :
|
`/etc/caddy/Caddyfile` :
|
||||||
|
|
||||||
```caddy
|
```caddy
|
||||||
exemple.o-k-i.net {
|
chatcontrol.o-k-i.net {
|
||||||
root * /srv/lage-chat-control
|
root * /srv/lage-chat-control
|
||||||
file_server
|
file_server
|
||||||
encode zstd gzip
|
encode zstd gzip
|
||||||
@@ -50,8 +72,11 @@ exemple.o-k-i.net {
|
|||||||
X-Content-Type-Options "nosniff"
|
X-Content-Type-Options "nosniff"
|
||||||
Referrer-Policy "no-referrer"
|
Referrer-Policy "no-referrer"
|
||||||
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||||
# CSP stricte : le site n'a aucune dépendance externe
|
# 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'"
|
# 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
|
```nginx
|
||||||
server {
|
server {
|
||||||
server_name exemple.o-k-i.net;
|
server_name chatcontrol.o-k-i.net;
|
||||||
root /srv/lage-chat-control;
|
root /srv/lage-chat-control;
|
||||||
index index.html;
|
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 / {
|
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/ {
|
location /_app/immutable/ {
|
||||||
@@ -88,7 +113,8 @@ server {
|
|||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header Referrer-Policy "no-referrer" always;
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" 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 on;
|
||||||
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
||||||
@@ -98,7 +124,7 @@ server {
|
|||||||
```sh
|
```sh
|
||||||
sudo ln -s /etc/nginx/sites-available/lage-chat-control /etc/nginx/sites-enabled/
|
sudo ln -s /etc/nginx/sites-available/lage-chat-control /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t && sudo systemctl reload nginx
|
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)
|
## 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
|
un runner Forgejo Actions sur labola peut exécuter build + rsync à chaque
|
||||||
push sur `main` (à ajouter quand le besoin se présente, pas avant).
|
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.
|
1. Installe `my_webapp` depuis le catalogue YunoHost avec le domaine
|
||||||
2. Copie le contenu de `frontend/build/` dans le dossier `www/` de l'app
|
`chatcontrol.o-k-i.net`, le chemin `/`, un accès **public** (visiteurs
|
||||||
(SFTP ou rsync avec l'utilisateur dédié créé par l'app).
|
compris), sans PHP ni base de données.
|
||||||
3. Dans la config nginx avancée de l'app, ajoute le bloc `try_files` ci-dessus.
|
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
|
## 6. Option D — Docker
|
||||||
|
|
||||||
@@ -144,8 +191,15 @@ majorité des cas.
|
|||||||
|
|
||||||
## 7. Vérifier après mise en ligne
|
## 7. Vérifier après mise en ligne
|
||||||
|
|
||||||
- [ ] `curl -sI https://exemple.o-k-i.net | grep -i content-security` → CSP présente
|
- [ ] `curl -s https://chatcontrol.o-k-i.net/outils/ | grep -o 'http-equiv="Content-Security-Policy"'`
|
||||||
- [ ] La page `/outils` répond (et `/outils?cat=messagerie` filtre)
|
→ 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
|
- [ ] DevTools > Application > Service worker : enregistré ; passer hors-ligne
|
||||||
et recharger une fiche déjà visitée → elle s'affiche
|
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
|
- [ ] observatory.mozilla.org : note A/A+ attendue avec les en-têtes ci-dessus
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
"prebuild": "node scripts/gen-sitemap.mjs",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Génère static/sitemap.xml au build (script `prebuild`) — remplace le
|
||||||
|
// sitemap qu'apportait @astrojs/sitemap à l'ère Astro. Zéro dépendance :
|
||||||
|
// les slugs sont relus depuis les frontmatters de content/outils/.
|
||||||
|
import { readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const SITE_URL = 'https://chatcontrol.o-k-i.net'
|
||||||
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const OUTILS = path.resolve(ROOT, '..', 'content', 'outils')
|
||||||
|
|
||||||
|
// les valeurs du frontmatter sont encodées en JSON (cf. tools.server.ts)
|
||||||
|
function slugOf(file) {
|
||||||
|
const raw = readFileSync(path.join(OUTILS, file), 'utf8')
|
||||||
|
const m = raw.match(/^slug:\s*("(?:[^"\\]|\\.)*")/m)
|
||||||
|
return m ? JSON.parse(m[1]) : file.replace(/\.md$/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const slugs = readdirSync(OUTILS)
|
||||||
|
.filter((f) => f.endsWith('.md'))
|
||||||
|
.map(slugOf)
|
||||||
|
.sort((a, b) => a.localeCompare(b, 'fr'))
|
||||||
|
|
||||||
|
const lastmod = new Date().toISOString().slice(0, 10)
|
||||||
|
|
||||||
|
const entry = (loc, priority) => ` <url>
|
||||||
|
<loc>${SITE_URL}${loc}</loc>
|
||||||
|
<lastmod>${lastmod}</lastmod>
|
||||||
|
<priority>${priority}</priority>
|
||||||
|
</url>`
|
||||||
|
|
||||||
|
const urls = [
|
||||||
|
entry('/', '1.0'),
|
||||||
|
entry('/outils/', '0.8'),
|
||||||
|
...slugs.map((slug) => entry(`/outils/${slug}/`, '0.6')),
|
||||||
|
entry('/quiz/', '0.5'),
|
||||||
|
entry('/contribuer/', '0.5')
|
||||||
|
]
|
||||||
|
|
||||||
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
${urls.join('\n')}
|
||||||
|
</urlset>
|
||||||
|
`
|
||||||
|
|
||||||
|
writeFileSync(path.join(ROOT, 'static', 'sitemap.xml'), xml)
|
||||||
|
console.log(`sitemap.xml : ${urls.length} URLs (lastmod ${lastmod})`)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment'
|
import { browser } from '$app/environment'
|
||||||
|
import { base } from '$app/paths'
|
||||||
import { page } from '$app/state'
|
import { page } from '$app/state'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
let current = $derived(browser ? page.url.searchParams.get('cat') : null)
|
let current = $derived(browser ? page.url.searchParams.get('cat') : null)
|
||||||
|
|
||||||
function href(slug: string | null): string {
|
function href(slug: string | null): string {
|
||||||
return slug ? `/outils?cat=${slug}` : '/outils'
|
return slug ? `${base}/outils?cat=${slug}` : `${base}/outils`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
import { SITE_URL } from '$lib/site'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** titre court de la page (« Annuaire des outils ») — omis sur l'accueil */
|
||||||
|
title?: string
|
||||||
|
description: string
|
||||||
|
/** chemin canonique avec slash final, ex. `/outils/` (trailingSlash: 'always') */
|
||||||
|
path: string
|
||||||
|
type?: 'website' | 'article'
|
||||||
|
}
|
||||||
|
|
||||||
|
let { title, description, path, type = 'website' }: Props = $props()
|
||||||
|
|
||||||
|
// même format que l'ère actuelle : « Titre — Lagé Chat Control »,
|
||||||
|
// et « Lagé Chat Control — accroche » sur l'accueil
|
||||||
|
let fullTitle = $derived(title ? `${title} — ${t('site.name')}` : `${t('site.name')} — ${t('site.tagline')}`)
|
||||||
|
let url = $derived(`${SITE_URL}${path}`)
|
||||||
|
// favicon.png servi en og:image — réellement 192×192 (vérifié au build)
|
||||||
|
const image = `${SITE_URL}/favicon.png`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{fullTitle}</title>
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<link rel="canonical" href={url} />
|
||||||
|
<meta property="og:type" content={type} />
|
||||||
|
<meta property="og:url" content={url} />
|
||||||
|
<meta property="og:title" content={fullTitle} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content={image} />
|
||||||
|
<meta property="og:image:width" content="192" />
|
||||||
|
<meta property="og:image:height" content="192" />
|
||||||
|
<meta property="og:image:alt" content={t('site.name')} />
|
||||||
|
<meta property="og:locale" content="fr_FR" />
|
||||||
|
<meta property="og:site_name" content={t('site.name')} />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:title" content={fullTitle} />
|
||||||
|
<meta name="twitter:description" content={description} />
|
||||||
|
<meta name="twitter:image" content={image} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
|
import { base } from '$app/paths'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
// Le clavier est roi sur desktop (doctrine §2.2.3) : raccourcis découvrables
|
// Le clavier est roi sur desktop (doctrine §2.2.3) : raccourcis découvrables
|
||||||
@@ -23,9 +24,9 @@
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
dialog.open ? dialog.close() : dialog.showModal()
|
dialog.open ? dialog.close() : dialog.showModal()
|
||||||
} else if (e.key === 'h') {
|
} else if (e.key === 'h') {
|
||||||
goto('/')
|
goto(`${base}/`)
|
||||||
} else if (e.key === 'a') {
|
} else if (e.key === 'a') {
|
||||||
goto('/outils')
|
goto(`${base}/outils`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { base } from '$app/paths'
|
||||||
import DifficultyBadge from './DifficultyBadge.svelte'
|
import DifficultyBadge from './DifficultyBadge.svelte'
|
||||||
import { isAdopted } from '$lib/adopted.svelte'
|
import { isAdopted } from '$lib/adopted.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
@@ -22,7 +23,7 @@
|
|||||||
<span class="monogram" style:--tool-color={tool.color} aria-hidden="true">
|
<span class="monogram" style:--tool-color={tool.color} aria-hidden="true">
|
||||||
{tool.monogram ?? tool.name.slice(0, 2)}
|
{tool.monogram ?? tool.name.slice(0, 2)}
|
||||||
</span>
|
</span>
|
||||||
<h3><a href="/outils/{tool.slug}">{tool.name}</a></h3>
|
<h3><a href="{base}/outils/{tool.slug}">{tool.name}</a></h3>
|
||||||
{#if isAdopted(tool.slug)}
|
{#if isAdopted(tool.slug)}
|
||||||
<!-- information périphérique (doctrine §3.5) : discret, jamais bloquant -->
|
<!-- information périphérique (doctrine §3.5) : discret, jamais bloquant -->
|
||||||
<span class="adopted" title={t('adopted.marked')}>
|
<span class="adopted" title={t('adopted.marked')}>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
/** Constantes du site — un seul endroit à changer si la forge bouge. */
|
/** Constantes du site — un seul endroit à changer si la forge bouge. */
|
||||||
export const REPO_URL = 'https://labola.o-k-i.net/cyber-mawonaj/lage-chat-control'
|
export const REPO_URL = 'https://labola.o-k-i.net/cyber-mawonaj/lage-chat-control'
|
||||||
|
|
||||||
|
/** Origine de production — base des URL absolues SEO (canonical, OG, sitemap). */
|
||||||
|
export const SITE_URL = 'https://chatcontrol.o-k-i.net'
|
||||||
export const NEW_TOOL_ISSUE_URL = `${REPO_URL}/issues/new?template=.gitea/ISSUE_TEMPLATE/nouvo-zouti.yaml`
|
export const NEW_TOOL_ISSUE_URL = `${REPO_URL}/issues/new?template=.gitea/ISSUE_TEMPLATE/nouvo-zouti.yaml`
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css'
|
import '../app.css'
|
||||||
import { onNavigate } from '$app/navigation'
|
import { onNavigate } from '$app/navigation'
|
||||||
|
import { base } from '$app/paths'
|
||||||
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte'
|
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte'
|
||||||
import OfflineBadge from '$lib/components/OfflineBadge.svelte'
|
import OfflineBadge from '$lib/components/OfflineBadge.svelte'
|
||||||
import ShortcutsDialog from '$lib/components/ShortcutsDialog.svelte'
|
import ShortcutsDialog from '$lib/components/ShortcutsDialog.svelte'
|
||||||
@@ -28,15 +29,15 @@
|
|||||||
|
|
||||||
<header class="center">
|
<header class="center">
|
||||||
<div class="cluster bar">
|
<div class="cluster bar">
|
||||||
<a class="brand" href="/">
|
<a class="brand" href="{base}/">
|
||||||
<span class="brand-mark" aria-hidden="true"></span>
|
<span class="brand-mark" aria-hidden="true"></span>
|
||||||
{t('site.name')}
|
{t('site.name')}
|
||||||
</a>
|
</a>
|
||||||
<nav aria-label={t('nav.home')}>
|
<nav aria-label={t('nav.home')}>
|
||||||
<ul class="cluster">
|
<ul class="cluster">
|
||||||
<li><a href="/outils">{t('nav.directory')}</a></li>
|
<li><a href="{base}/outils">{t('nav.directory')}</a></li>
|
||||||
<li><a href="/quiz">{t('quiz.nav')}</a></li>
|
<li><a href="{base}/quiz">{t('quiz.nav')}</a></li>
|
||||||
<li><a href="/contribuer">{t('nav.contribute')}</a></li>
|
<li><a href="{base}/contribuer">{t('nav.contribute')}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
// Tout le site est prérendu (ADR-001) : HTML statique servi par nginx,
|
// Tout le site est prérendu (ADR-001) : HTML statique servi par nginx,
|
||||||
// un lien fonctionne sans JS (doctrine §4.4).
|
// un lien fonctionne sans JS (doctrine §4.4).
|
||||||
export const prerender = true
|
export const prerender = true
|
||||||
|
|
||||||
|
// Chaque page est générée comme `<route>/index.html` (et non `<route>.html`) :
|
||||||
|
// les URL canoniques finissent par `/`, ce que tout serveur statique sert sans
|
||||||
|
// règle spéciale (nginx `try_files $uri/`, Apache mod_dir…) — et ça évite le
|
||||||
|
// conflit fichier/dossier documenté dans docs/APACHE_AUTOINDEX.md.
|
||||||
|
export const trailingSlash = 'always'
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { base } from '$app/paths'
|
||||||
|
import Seo from '$lib/components/Seo.svelte'
|
||||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||||
import { adoptedCount } from '$lib/adopted.svelte'
|
import { adoptedCount } from '$lib/adopted.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
@@ -9,17 +11,14 @@
|
|||||||
let jes = $derived(adoptedCount())
|
let jes = $derived(adoptedCount())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<Seo description={t('site.description')} path="/" />
|
||||||
<title>{t('site.name')} — {t('site.tagline')}</title>
|
|
||||||
<meta name="description" content={t('site.description')} />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<!-- Valeur livrée en < 5 s (doctrine §2.1) : le héros dit quoi, pour qui,
|
<!-- Valeur livrée en < 5 s (doctrine §2.1) : le héros dit quoi, pour qui,
|
||||||
et donne UNE action primaire (Von Restorff, doctrine §1.2). -->
|
et donne UNE action primaire (Von Restorff, doctrine §1.2). -->
|
||||||
<section class="hero stack">
|
<section class="hero stack">
|
||||||
<h1>{t('hero.title')}</h1>
|
<h1>{t('hero.title')}</h1>
|
||||||
<p class="sub">{t('hero.subtitle')}</p>
|
<p class="sub">{t('hero.subtitle')}</p>
|
||||||
<p><a class="cta" href="/outils">{t('hero.cta')} · {data.toolCount} {t('directory.results')}</a></p>
|
<p><a class="cta" href="{base}/outils">{t('hero.cta')} · {data.toolCount} {t('directory.results')}</a></p>
|
||||||
{#if jes > 0}
|
{#if jes > 0}
|
||||||
<p class="jes" role="status">
|
<p class="jes" role="status">
|
||||||
<span class="jes-mark" aria-hidden="true">✓</span>
|
<span class="jes-mark" aria-hidden="true">✓</span>
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
<ul class="cluster">
|
<ul class="cluster">
|
||||||
{#each arc.categories as cat (cat.slug)}
|
{#each arc.categories as cat (cat.slug)}
|
||||||
<li>
|
<li>
|
||||||
<a href="/outils?cat={cat.slug}">
|
<a href="{base}/outils?cat={cat.slug}">
|
||||||
{cat.title}
|
{cat.title}
|
||||||
<span class="count">{cat.count}</span>
|
<span class="count">{cat.count}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Seo from '$lib/components/Seo.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
import { NEW_TOOL_ISSUE_URL, REPO_URL } from '$lib/site'
|
import { NEW_TOOL_ISSUE_URL, REPO_URL } from '$lib/site'
|
||||||
|
|
||||||
@@ -10,10 +11,7 @@
|
|||||||
] as const
|
] as const
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<Seo title={t('contrib.title')} description={t('contrib.intro')} path="/contribuer/" />
|
||||||
<title>{t('contrib.title')} — {t('site.name')}</title>
|
|
||||||
<meta name="description" content={t('contrib.intro')} />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<header class="stack">
|
<header class="stack">
|
||||||
<h1>{t('contrib.title')}</h1>
|
<h1>{t('contrib.title')}</h1>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment'
|
import { browser } from '$app/environment'
|
||||||
|
import { base } from '$app/paths'
|
||||||
import { page } from '$app/state'
|
import { page } from '$app/state'
|
||||||
import CategoryNav from '$lib/components/CategoryNav.svelte'
|
import CategoryNav from '$lib/components/CategoryNav.svelte'
|
||||||
import SearchBar from '$lib/components/SearchBar.svelte'
|
import SearchBar from '$lib/components/SearchBar.svelte'
|
||||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||||
import EndMarker from '$lib/components/EndMarker.svelte'
|
import EndMarker from '$lib/components/EndMarker.svelte'
|
||||||
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
||||||
|
import Seo from '$lib/components/Seo.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
let { data } = $props()
|
let { data } = $props()
|
||||||
@@ -38,14 +40,11 @@
|
|||||||
if (d) params.set('diff', d)
|
if (d) params.set('diff', d)
|
||||||
else params.delete('diff')
|
else params.delete('diff')
|
||||||
const qs = params.toString()
|
const qs = params.toString()
|
||||||
return qs ? `/outils?${qs}` : '/outils'
|
return qs ? `${base}/outils?${qs}` : `${base}/outils`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<Seo title={t('directory.title')} description={t('directory.intro')} path="/outils/" />
|
||||||
<title>{t('directory.title')} — {t('site.name')}</title>
|
|
||||||
<meta name="description" content={t('directory.intro')} />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<header class="stack">
|
<header class="stack">
|
||||||
<h1>{t('directory.title')}</h1>
|
<h1>{t('directory.title')}</h1>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { base } from '$app/paths'
|
||||||
import AdoptButton from '$lib/components/AdoptButton.svelte'
|
import AdoptButton from '$lib/components/AdoptButton.svelte'
|
||||||
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
||||||
|
import Seo from '$lib/components/Seo.svelte'
|
||||||
import { getLocale, t } from '$lib/i18n/index.svelte'
|
import { getLocale, t } from '$lib/i18n/index.svelte'
|
||||||
|
|
||||||
let { data } = $props()
|
let { data } = $props()
|
||||||
@@ -33,17 +35,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{tool.name} — {t('site.name')}</title>
|
|
||||||
<meta name="description" content={tool.excerpt} />
|
|
||||||
<meta property="og:title" content={tool.name} />
|
|
||||||
<meta property="og:description" content={tool.excerpt} />
|
|
||||||
<meta property="og:type" content="article" />
|
|
||||||
<!-- eslint-disable-next-line svelte/no-at-html-tags — JSON-LD généré au build depuis nos propres données -->
|
<!-- eslint-disable-next-line svelte/no-at-html-tags — JSON-LD généré au build depuis nos propres données -->
|
||||||
{@html jsonLd}
|
{@html jsonLd}
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<Seo title={tool.name} description={tool.excerpt} path="/outils/{tool.slug}/" type="article" />
|
||||||
|
|
||||||
<nav aria-label={t('tool.backToDirectory')}>
|
<nav aria-label={t('tool.backToDirectory')}>
|
||||||
<a href="/outils">← {t('tool.backToDirectory')}</a>
|
<a href="{base}/outils">← {t('tool.backToDirectory')}</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<article class="stack" style="--gap: var(--space-4)">
|
<article class="stack" style="--gap: var(--space-4)">
|
||||||
@@ -57,7 +56,7 @@
|
|||||||
<div class="cluster meta">
|
<div class="cluster meta">
|
||||||
<DifficultyBadge difficulty={tool.difficulty} />
|
<DifficultyBadge difficulty={tool.difficulty} />
|
||||||
{#if data.category}
|
{#if data.category}
|
||||||
<a href="/outils?cat={data.category.slug}">{data.category.title}</a>
|
<a href="{base}/outils?cat={data.category.slug}">{data.category.title}</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if tool.license}
|
{#if tool.license}
|
||||||
<span>{t('tool.license')} : {tool.license}</span>
|
<span>{t('tool.license')} : {tool.license}</span>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { base } from '$app/paths'
|
||||||
|
import Seo from '$lib/components/Seo.svelte'
|
||||||
import { t } from '$lib/i18n/index.svelte'
|
import { t } from '$lib/i18n/index.svelte'
|
||||||
import type { QuizContent } from '$lib/content/quiz.server'
|
import type { QuizContent } from '$lib/content/quiz.server'
|
||||||
|
|
||||||
@@ -107,10 +109,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<Seo title={t('quiz.title')} description={t('quiz.lede')} path="/quiz/" />
|
||||||
<title>{t('quiz.title')} — {t('site.name')}</title>
|
|
||||||
<meta name="description" content={t('quiz.lede')} />
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<header class="stack">
|
<header class="stack">
|
||||||
<p class="eyebrow">{t('quiz.eyebrow')}</p>
|
<p class="eyebrow">{t('quiz.eyebrow')}</p>
|
||||||
@@ -119,7 +118,7 @@
|
|||||||
<p class="privacy">{t('quiz.privacyNote')}</p>
|
<p class="privacy">{t('quiz.privacyNote')}</p>
|
||||||
<p class="cluster">
|
<p class="cluster">
|
||||||
<a class="cta" href="#quiz-form">{t('quiz.start')}</a>
|
<a class="cta" href="#quiz-form">{t('quiz.start')}</a>
|
||||||
<a class="secondary" href="/outils">{t('quiz.readDirectory')}</a>
|
<a class="secondary" href="{base}/outils">{t('quiz.readDirectory')}</a>
|
||||||
</p>
|
</p>
|
||||||
<noscript>
|
<noscript>
|
||||||
<p class="privacy">{t('quiz.privacyNote')} — JavaScript est requis pour calculer ton score.</p>
|
<p class="privacy">{t('quiz.privacyNote')} — JavaScript est requis pour calculer ton score.</p>
|
||||||
@@ -196,7 +195,7 @@
|
|||||||
<li class="rec-card stack">
|
<li class="rec-card stack">
|
||||||
<strong>{r.question.label}</strong>
|
<strong>{r.question.label}</strong>
|
||||||
<p class="prose">{r.question.rec}</p>
|
<p class="prose">{r.question.rec}</p>
|
||||||
<a href="/outils?cat={r.question.category}">{t('quiz.openDirectory')}</a>
|
<a href="{base}/outils?cat={r.question.category}">{t('quiz.openDirectory')}</a>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -231,7 +230,7 @@
|
|||||||
<li class="cat-card stack">
|
<li class="cat-card stack">
|
||||||
<strong>{q.label}</strong>
|
<strong>{q.label}</strong>
|
||||||
<p class="prose">{q.rec}</p>
|
<p class="prose">{q.rec}</p>
|
||||||
<a href="/outils?cat={q.category}">{t('quiz.openDirectory')}</a>
|
<a href="{base}/outils?cat={q.category}">{t('quiz.openDirectory')}</a>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -8,12 +8,17 @@
|
|||||||
* - assets versionnés (/_app/immutable/…) : cache-first, immuables ;
|
* - assets versionnés (/_app/immutable/…) : cache-first, immuables ;
|
||||||
* - pages HTML : network-first avec repli cache — chaque page visitée
|
* - pages HTML : network-first avec repli cache — chaque page visitée
|
||||||
* devient disponible hors-ligne (« cache des fiches consultées ») ;
|
* devient disponible hors-ligne (« cache des fiches consultées ») ;
|
||||||
* - coquille (/, /outils, polices, manifest) : précachée à l'installation.
|
* - coquille (pages prérendues, assets, manifest) : précachée à l'installation.
|
||||||
*/
|
*/
|
||||||
import { build, files, version } from '$service-worker'
|
import { build, files, prerendered, version } from '$service-worker'
|
||||||
|
|
||||||
|
// $app/paths n'est pas importable dans un service worker : la base se déduit
|
||||||
|
// de l'URL du script lui-même, servi sous `${base}/service-worker.js`.
|
||||||
|
const base = new URL('.', self.location.href).pathname.replace(/\/$/, '')
|
||||||
|
|
||||||
const CACHE = `oki-${version}`
|
const CACHE = `oki-${version}`
|
||||||
const SHELL = ['/', '/outils', '/quiz', '/contribuer', ...build, ...files]
|
// `prerendered` liste toutes les pages prérendues, préfixe base déjà inclus
|
||||||
|
const SHELL = [...prerendered, ...build, ...files]
|
||||||
|
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
@@ -40,7 +45,7 @@ self.addEventListener('fetch', (event) => {
|
|||||||
if (url.origin !== self.location.origin) return
|
if (url.origin !== self.location.origin) return
|
||||||
|
|
||||||
// assets au nom haché : jamais modifiés, cache-first
|
// assets au nom haché : jamais modifiés, cache-first
|
||||||
if (url.pathname.startsWith('/_app/immutable/')) {
|
if (url.pathname.startsWith(`${base}/_app/immutable/`)) {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(request).then((hit) => hit ?? fetch(request))
|
caches.match(request).then((hit) => hit ?? fetch(request))
|
||||||
)
|
)
|
||||||
@@ -61,7 +66,7 @@ self.addEventListener('fetch', (event) => {
|
|||||||
// navigation vers une page jamais visitée : renvoyer l'accueil
|
// navigation vers une page jamais visitée : renvoyer l'accueil
|
||||||
// plutôt qu'une erreur réseau brute
|
// plutôt qu'une erreur réseau brute
|
||||||
if (request.mode === 'navigate') {
|
if (request.mode === 'navigate') {
|
||||||
const home = await cache.match('/')
|
const home = await cache.match(`${base}/`)
|
||||||
if (home) return home
|
if (home) return home
|
||||||
}
|
}
|
||||||
throw new Error('offline')
|
throw new Error('offline')
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
* Polices auto-hébergées, servies depuis notre domaine — jamais de Google
|
* Polices auto-hébergées, servies depuis notre domaine — jamais de Google
|
||||||
* Fonts (doctrine §5.3 : performance, RGPD, souveraineté).
|
* Fonts (doctrine §5.3 : performance, RGPD, souveraineté).
|
||||||
* Budget : 2 fichiers WOFF2 maximum. Le gras du corps est synthétisé.
|
* Budget : 2 fichiers WOFF2 maximum. Le gras du corps est synthétisé.
|
||||||
|
* Fichiers dans src/lib/assets/fonts : Vite les hache sous _app/immutable,
|
||||||
|
* l'URL reste donc correcte quel que soit paths.base.
|
||||||
*/
|
*/
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Atkinson Hyperlegible';
|
font-family: 'Atkinson Hyperlegible';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/fonts/atkinson-hyperlegible-latin-400-normal.woff2') format('woff2');
|
src: url('../lib/assets/fonts/atkinson-hyperlegible-latin-400-normal.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
@@ -16,5 +18,5 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url('/fonts/fraunces-latin-700-normal.woff2') format('woff2');
|
src: url('../lib/assets/fonts/fraunces-latin-700-normal.woff2') format('woff2');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://chatcontrol.o-k-i.net/sitemap.xml
|
||||||
|
|||||||
@@ -3,13 +3,13 @@
|
|||||||
"short_name": "Lagé CC",
|
"short_name": "Lagé CC",
|
||||||
"description": "Annuaire d'outils libres pour la souveraineté numérique",
|
"description": "Annuaire d'outils libres pour la souveraineté numérique",
|
||||||
"lang": "fr",
|
"lang": "fr",
|
||||||
"start_url": "/",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#17181f",
|
"background_color": "#17181f",
|
||||||
"theme_color": "#17181f",
|
"theme_color": "#17181f",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/favicon.png",
|
"src": "favicon.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any"
|
"purpose": "any"
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.8</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/aegis/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/alias/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/backups/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/bitchat/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/bitcoin/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/bitwarden/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/brave/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/briar/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/cloudai/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/cloudflare/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/cryptomator/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/databrokers/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/element/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/fediverse/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/firefox/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/grapheneos/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/ivpn/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/keepassxc/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/linux/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/localai/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/mailbox/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/maps/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/mastodon/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/molly/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/monero/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/mullvad/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/mullvadbrowser/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/mullvaddns/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/nc-storage/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/nextcloud/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/nostr/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/notes/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/photos/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/pihole/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/pocketpal/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/protondrive/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/protonmail/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/protonpass/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/protonvpn/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/quad9/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/qubes/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/search/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/session/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/signal/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/simplex/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/synapse/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/tails/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/tailscale/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/tor/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/tuta/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/ublock/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/visio/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/website/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/yubikey/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/outils/yunohost/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.6</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/quiz/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://chatcontrol.o-k-i.net/contribuer/</loc>
|
||||||
|
<lastmod>2026-07-21</lastmod>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
@@ -10,6 +10,26 @@ const config = {
|
|||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
fallback: '404.html'
|
fallback: '404.html'
|
||||||
}),
|
}),
|
||||||
|
// Déployé à la racine de https://chatcontrol.o-k-i.net/ (YunoHost) :
|
||||||
|
// pas de paths.base. Pour servir sous un sous-chemin, ajouter ici
|
||||||
|
// paths: { base: '/prefixe' } et adapter les règles du serveur.
|
||||||
|
// Le script de démarrage SvelteKit est inline (il embarque les données de
|
||||||
|
// la page) : une CSP `script-src 'self'` posée en en-tête HTTP le bloque
|
||||||
|
// et tue l'hydratation (recherche et filtres morts — vécu sur o2switch).
|
||||||
|
// En mode hash, chaque page prérendue porte une <meta> CSP avec l'empreinte
|
||||||
|
// de SON script inline. L'en-tête serveur ne doit donc plus fixer
|
||||||
|
// script-src (voir DEPLOYMENT.md §2/§3).
|
||||||
|
csp: {
|
||||||
|
mode: 'hash',
|
||||||
|
directives: {
|
||||||
|
'script-src': ['self']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Un seul bundle JS + une seule feuille CSS au lieu de ~30 chunks :
|
||||||
|
// moins de requêtes par page (3G, anti-flood o2switch qui répond 429).
|
||||||
|
output: {
|
||||||
|
bundleStrategy: 'single'
|
||||||
|
},
|
||||||
prerender: {
|
prerender: {
|
||||||
handleHttpError: 'fail'
|
handleHttpError: 'fail'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user