Compare commits
2 Commits
f4e09ca886
...
b13a1e7a31
| Author | SHA1 | Date | |
|---|---|---|---|
| b13a1e7a31 | |||
| db5a997e50 |
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "oki-preview",
|
||||
"runtimeExecutable": "pnpm",
|
||||
"runtimeArgs": ["--dir", "frontend", "preview", "--port", "4173"],
|
||||
"port": 4173
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
name: "Nouvo zouti — Proposer un outil"
|
||||
description: "Pwopozé on zouti lib pou lis-la / Proposer un outil libre pour l'annuaire"
|
||||
title: "[zouti] "
|
||||
labels:
|
||||
- "nouvo-zouti"
|
||||
body:
|
||||
- type: input
|
||||
id: nom
|
||||
attributes:
|
||||
label: "Non a zouti-la / Nom de l'outil"
|
||||
placeholder: "ex. Signal"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: "Sit ofisyèl / Site officiel"
|
||||
placeholder: "https://…"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: licence
|
||||
attributes:
|
||||
label: "Licence (identifiant SPDX si tu le connais)"
|
||||
placeholder: "ex. AGPL-3.0"
|
||||
- type: dropdown
|
||||
id: difficulte
|
||||
attributes:
|
||||
label: "Difikilté / Difficulté d'installation"
|
||||
options:
|
||||
- "🟢 Débitan / Débutant"
|
||||
- "🟡 Entèmédia / Intermédiaire"
|
||||
- "🔴 Avansé / Avancé"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: pourquoi
|
||||
attributes:
|
||||
label: "Poukisa i konté ? / Pourquoi il compte ?"
|
||||
description: "En quoi cet outil sert la souveraineté numérique ? Qui devrait s'en servir ?"
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: verifs
|
||||
attributes:
|
||||
label: "Vérifications"
|
||||
options:
|
||||
- label: "L'outil est libre / open-source (code source publié)"
|
||||
required: true
|
||||
- label: "J'ai vérifié qu'il n'est pas déjà dans l'annuaire"
|
||||
required: true
|
||||
@@ -5,3 +5,7 @@ playwright-report/
|
||||
test-results/
|
||||
*.local
|
||||
.DS_Store
|
||||
|
||||
# SvelteKit (frontend/)
|
||||
.svelte-kit/
|
||||
frontend/build/
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# ADR-001 — Backend : SvelteKit natif (fichiers plats), pas de Strapi
|
||||
|
||||
**Statut** : accepté · **Date** : 2026-07-10 · **Décideur** : migration OKI d'exitchatcontrol.org
|
||||
**Doctrine de référence** : `ui-ux-souveraine-oki.md` (citée « doctrine §x.y » ci-dessous)
|
||||
|
||||
## Contexte
|
||||
|
||||
exitchatcontrol.org est un annuaire de ~55 outils libres répartis en 23 sections
|
||||
thématiques, actuellement généré statiquement par Astro à partir de fichiers
|
||||
versionnés (`src/data/*.json`, `src/content/sections/*/*.mdx`). Le déploiement
|
||||
cible est un conteneur nginx auto-hébergé (YunoHost/Docker), sans backend.
|
||||
|
||||
Deux options étaient sur la table (mission §2.1) :
|
||||
|
||||
- **Option A** : Strapi v5 headless, base relationnelle, admin web, API, i18n plugin.
|
||||
- **Option B** : SvelteKit full-stack, contenu en fichiers Markdown/JSON versionnés,
|
||||
parsés au build, site prérendu.
|
||||
|
||||
## Décision
|
||||
|
||||
**Option B — SvelteKit 2 + Svelte 5, contenu en fichiers plats prérendus.**
|
||||
|
||||
## Justification par la règle de décision
|
||||
|
||||
La règle imposée : minimiser la charge cognitive **extrinsèque** (doctrine §1.2,
|
||||
Sweller) pour l'utilisateur final ET l'administrateur, dans le budget performance
|
||||
(doctrine §4.4).
|
||||
|
||||
1. **Les données sont déjà statiques et versionnées.** 55 outils, mises à jour
|
||||
par pull request espacées de plusieurs jours/semaines (historique git). Le seuil
|
||||
« le volume de données justifie une base relationnelle » posé comme condition
|
||||
de l'option A n'est pas atteint — 55 fiches tiennent dans un dossier de
|
||||
fichiers Markdown lisibles à l'œil nu.
|
||||
2. **Charge d'administration.** Strapi = un service Node persistant + une base +
|
||||
des migrations de schéma + des mises à jour de sécurité + des sauvegardes.
|
||||
Pour l'admin OKI, c'est de la charge extrinsèque pure : la tâche réelle
|
||||
(« corriger une fiche ») devient « se connecter à un admin, naviguer un CMS,
|
||||
publier, invalider un cache ». En fichiers plats : éditer un fichier, commit.
|
||||
Le git devient l'historique éditorial, la revue par PR devient la modération.
|
||||
3. **Budget performance (doctrine §4.4).** Site 100 % prérendu : premier rendu
|
||||
= HTML servi par nginx, zéro requête API, JS initial minimal (hydratation
|
||||
sélective). Strapi n'aiderait pas mais ajouterait un point de latence et de
|
||||
panne si l'on cédait à la tentation du rendu à la demande.
|
||||
4. **Souveraineté et surface d'attaque.** Un site statique n'a pas de CVE de CMS,
|
||||
pas de compte admin à voler, pas de base à exfiltrer. C'est la posture
|
||||
cohérente avec le sujet même du site (OPSEC, résistance à la censure) :
|
||||
le site doit être **mirrorable** (un tarball du build suffit), ce qu'un
|
||||
Strapi rend impossible.
|
||||
5. **Loi de Tesler (doctrine §1.3)** : la complexité éditoriale n'est pas
|
||||
supprimée, elle est déplacée vers le format de fichier. On l'absorbe par un
|
||||
frontmatter strict + validation au build (le build échoue si une fiche est
|
||||
invalide — équivalent des types du CMS, sans le CMS).
|
||||
|
||||
### Ce qui ferait rebasculer vers Strapi (critères de réversibilité)
|
||||
|
||||
- Une équipe éditoriale non-technique de plusieurs personnes publiant chaque semaine ;
|
||||
- Des soumissions communautaires en volume nécessitant un workflow draft/publish ;
|
||||
- Plus de ~500 fiches ou des relations réellement complexes.
|
||||
|
||||
Dans ce cas, le modèle de données de la mission §3 s'applique tel quel ; le
|
||||
frontend SvelteKit resterait inchangé (il consommerait l'API au build).
|
||||
Étape intermédiaire possible sans backend : Decap CMS (git-based) sur le dossier
|
||||
`content/` — écarté pour l'instant, « préférer la solution la plus simple ».
|
||||
|
||||
## Décisions techniques associées
|
||||
|
||||
- **adapter-static + prerender total** : parité avec le déploiement nginx actuel
|
||||
(Dockerfile conservé dans l'esprit). Un lien fonctionne sans JS (doctrine §4.4).
|
||||
- **Markdown + frontmatter maison** : parseur frontmatter minimal (~40 lignes,
|
||||
zéro dépendance) + `marked` (MIT) pour le corps, exécuté **au build uniquement**
|
||||
— rien de tout cela n'est expédié au client.
|
||||
- **i18n maison typé** (pattern repris du projet source) plutôt que
|
||||
Paraglide/inlang : ~40 lignes, dictionnaires JSON `fr`/`gcf` vérifiés par le
|
||||
compilateur TypeScript (une clé manquante = erreur de build). Paraglide reste
|
||||
la piste si le nombre de locales croît.
|
||||
- **Langues** : `fr` (défaut) + `gcf` (créole guadeloupéen, code ISO 639-3
|
||||
enregistré BCP 47 valide). Micro-textes créoles d'abord (doctrine §4.2 :
|
||||
impact affectif disproportionné pour un coût faible) ; traduction intégrale
|
||||
des fiches = chantier éditorial séparé (voir `docs/CONTENT_GUIDE.md`).
|
||||
- **CSS natif en `@layer`** : `reset → tokens → base → layouts → components →
|
||||
utilities` (doctrine §5.2), tokens `oklch()`, container queries, `clamp()`.
|
||||
Pas de Tailwind (doctrine §5.2 option 4 : écarté en connaissance de cause).
|
||||
- **Licence du code** : MIT, comme le projet source (continuité).
|
||||
@@ -0,0 +1,92 @@
|
||||
[
|
||||
{
|
||||
"slug": "messagerie",
|
||||
"title": "Messagerie chiffrée",
|
||||
"order": 3,
|
||||
"part": 1
|
||||
},
|
||||
{
|
||||
"slug": "email",
|
||||
"title": "E-mail chiffré & PGP",
|
||||
"order": 4,
|
||||
"part": 2
|
||||
},
|
||||
{
|
||||
"slug": "navigateur",
|
||||
"title": "Navigateur & recherche",
|
||||
"order": 5,
|
||||
"part": 3
|
||||
},
|
||||
{
|
||||
"slug": "dns",
|
||||
"title": "DNS chiffré & Cloudflare",
|
||||
"order": 6,
|
||||
"part": 3
|
||||
},
|
||||
{
|
||||
"slug": "vpn",
|
||||
"title": "VPN : la vérité",
|
||||
"order": 7,
|
||||
"part": 4
|
||||
},
|
||||
{
|
||||
"slug": "stockage",
|
||||
"title": "Stockage chiffré",
|
||||
"order": 10,
|
||||
"part": 6
|
||||
},
|
||||
{
|
||||
"slug": "motsdepasse",
|
||||
"title": "Gestionnaire de mots de passe",
|
||||
"order": 11,
|
||||
"part": 7
|
||||
},
|
||||
{
|
||||
"slug": "deuxfa",
|
||||
"title": "Double authentification & clés matérielles",
|
||||
"order": 12,
|
||||
"part": 7
|
||||
},
|
||||
{
|
||||
"slug": "social",
|
||||
"title": "Réseaux sociaux décentralisés",
|
||||
"order": 13,
|
||||
"part": 8
|
||||
},
|
||||
{
|
||||
"slug": "argent",
|
||||
"title": "Souveraineté financière",
|
||||
"order": 14,
|
||||
"part": 9
|
||||
},
|
||||
{
|
||||
"slug": "ia",
|
||||
"title": "L'IA conversationnelle",
|
||||
"order": 15,
|
||||
"part": 9
|
||||
},
|
||||
{
|
||||
"slug": "boiteaoutils",
|
||||
"title": "La boîte à outils du quotidien",
|
||||
"order": 16,
|
||||
"part": 9
|
||||
},
|
||||
{
|
||||
"slug": "selfhost",
|
||||
"title": "Auto-hébergement",
|
||||
"order": 17,
|
||||
"part": 10
|
||||
},
|
||||
{
|
||||
"slug": "tor",
|
||||
"title": "Tor",
|
||||
"order": 18,
|
||||
"part": 11
|
||||
},
|
||||
{
|
||||
"slug": "os",
|
||||
"title": "Systèmes d'exploitation libres",
|
||||
"order": 19,
|
||||
"part": 12
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Aegis · Ente Auth"
|
||||
slug: "aegis"
|
||||
category: "deuxfa"
|
||||
difficulty: "beginner"
|
||||
license: "GPL-3.0"
|
||||
url: "https://getaegis.app"
|
||||
color: "#2c5c9c"
|
||||
monogram: "Ae"
|
||||
iconSlug: "aegisauthenticator"
|
||||
tags: ["Codes TOTP","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des applications qui génèrent les codes à six chiffres qui changent toutes les 30 secondes, en
|
||||
remplacement de Google Authenticator.
|
||||
|
||||
## why
|
||||
|
||||
<b>Aegis</b> (Android) garde vos codes dans un coffre chiffré local, sans cloud.{' '}
|
||||
<b>Ente Auth</b> ajoute une synchronisation chiffrée de bout en bout entre appareils, et a été
|
||||
audité. Bien plus sûrs que le SMS, et gratuits.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde : c'est le niveau de 2FA à adopter par défaut, avant les clés matérielles.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez Aegis (F-Droid) ou Ente Auth (toutes plateformes).</li>
|
||||
<li>
|
||||
Sur chaque compte, activez la 2FA « application d'authentification » et scannez le QR code
|
||||
affiché.
|
||||
</li>
|
||||
<li>
|
||||
Notez les <b>codes de secours</b> sur papier et faites une sauvegarde chiffrée de votre
|
||||
coffre.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: "SimpleLogin · addy.io"
|
||||
slug: "alias"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "beginner"
|
||||
url: "https://simplelogin.io"
|
||||
color: "#c0392b"
|
||||
monogram: "@"
|
||||
iconSlug: "simplelogin"
|
||||
tags: ["Alias e-mail","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des alias e-mail jetables : une adresse unique par site, qui redirige vers votre vraie boîte
|
||||
sans jamais la révéler.
|
||||
|
||||
## why
|
||||
|
||||
Vous ne donnez plus votre vraie adresse partout. Si un site est piraté ou vous spamme, vous
|
||||
coupez l'alias correspondant, et vous savez immédiatement qui a fait fuiter vos données.{' '}
|
||||
<b>SimpleLogin</b> est intégré à Proton ; <b>addy.io</b> propose des alias illimités dès l'offre
|
||||
gratuite.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, et particulièrement les comptes pseudonymes qui veulent cloisonner leurs
|
||||
inscriptions.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Créez un compte, installez l'extension de navigateur.</li>
|
||||
<li>À chaque inscription, générez un nouvel alias au lieu de saisir votre vraie adresse.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Sauvegardes chiffrées"
|
||||
slug: "backups"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "intermediate"
|
||||
url: "https://cryptomator.org"
|
||||
color: "#5a6570"
|
||||
monogram: "Bk"
|
||||
iconSlug: "cryptomator"
|
||||
tags: ["Cryptomator · restic","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
De quoi sauvegarder votre téléphone et votre ordinateur en chiffrant les données <b>avant</b>{' '}
|
||||
qu'elles ne partent dans un cloud.
|
||||
|
||||
## why
|
||||
|
||||
Les sauvegardes iCloud/Google sont souvent accessibles au fournisseur, donc à la justice.{' '}
|
||||
<b>Cryptomator</b> chiffre vos fichiers avant de les envoyer sur n'importe quel cloud ;{' '}
|
||||
<b>restic</b> fait des sauvegardes chiffrées automatiques ; <b>Proton Drive</b> est chiffré de
|
||||
bout en bout. Sur iPhone, activez la « Protection avancée des données » pour chiffrer vos
|
||||
sauvegardes iCloud.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde devrait chiffrer ses sauvegardes. Règle d'or : deux copies chiffrées, sur deux
|
||||
supports, en deux lieux.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
iPhone : Réglages > votre nom > iCloud > Protection avancée des données : activez-la.
|
||||
</li>
|
||||
<li>
|
||||
PC : installez Cryptomator, créez un coffre, placez-y vos fichiers avant de synchroniser vers
|
||||
le cloud.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "Bitchat"
|
||||
slug: "bitchat"
|
||||
category: "messagerie"
|
||||
difficulty: "advanced"
|
||||
url: "https://bitchat.free"
|
||||
color: "#e8871e"
|
||||
monogram: "bit"
|
||||
tags: ["Mesh Bluetooth","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
La messagerie mesh de Jack Dorsey : les téléphones proches se relaient les messages de proche en
|
||||
proche, sans internet ni serveur ni compte.
|
||||
|
||||
## why
|
||||
|
||||
Chiffrement de bout en bout (AES-256-GCM) et un « mode panique » qui efface tout en trois
|
||||
tapotements sur le logo.
|
||||
|
||||
## who
|
||||
|
||||
Manifestations et blackout, en complément de Briar. À utiliser avec prudence.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez depuis l'App Store (iOS) ou l'APK/GitHub (Android).</li>
|
||||
<li>Ouvrez l'application et choisissez un pseudo. Aucun compte à créer.</li>
|
||||
<li>Les appareils proches se découvrent automatiquement en Bluetooth.</li>
|
||||
<li>En cas de danger, triple-tapez le logo pour tout effacer (mode panique).</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "Bitcoin (auto-conservation)"
|
||||
slug: "bitcoin"
|
||||
category: "argent"
|
||||
difficulty: "intermediate"
|
||||
url: "https://bitcoin.org"
|
||||
color: "#f7931a"
|
||||
monogram: "₿"
|
||||
iconSlug: "bitcoin"
|
||||
tags: ["Résistant à la censure"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une monnaie numérique que vous détenez vous-même, sans passer par une banque.
|
||||
|
||||
## why
|
||||
|
||||
En <b>auto-conservation</b> (vos clés dans un portefeuille que vous seul contrôlez), personne ne
|
||||
peut geler ni bloquer vos fonds. Sa confidentialité n'est pas parfaite (le registre est public),
|
||||
mais il échappe à la censure bancaire, utile le jour où un paiement est refusé.
|
||||
|
||||
## who
|
||||
|
||||
Se prémunir contre le gel de compte ou la dé-bancarisation, et payer anonymement un service
|
||||
comme un VPN.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Procurez-vous des bitcoins (plateforme d'échange, ou entre particuliers).</li>
|
||||
<li>
|
||||
Transférez-les aussitôt vers un portefeuille que <b>vous</b> contrôlez : un portefeuille
|
||||
matériel (Trezor, ColdCard) ou une application libre. Ne les laissez pas sur la plateforme.
|
||||
</li>
|
||||
<li>Sauvegardez votre phrase de récupération sur papier, jamais en photo ni en ligne.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: "Bitwarden"
|
||||
slug: "bitwarden"
|
||||
category: "motsdepasse"
|
||||
difficulty: "beginner"
|
||||
license: "GPL-3.0"
|
||||
url: "https://bitwarden.com"
|
||||
color: "#175ddc"
|
||||
monogram: "Bw"
|
||||
iconSlug: "bitwarden"
|
||||
tags: ["Le meilleur pour débuter","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un coffre-fort à mots de passe qui les mémorise, les génère et les remplit pour toi, sur tous tes appareils.
|
||||
|
||||
## why
|
||||
|
||||
Gratuit, audité, multiplateforme, chiffré de bout en bout. Tu n'as plus qu'un seul mot de passe fort à retenir — et le jour où tu veux ne dépendre de personne, il est <b>auto-hébergeable</b> (via Vaultwarden).
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. C'est la fondation de toute ta sécurité : un mot de passe unique et fort par service, sans effort de mémoire.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Crée un compte sur bitwarden.com et choisis un mot de passe maître <b>long</b> — une phrase entière. Ne l'oublie jamais : il n'est pas récupérable, et c'est voulu.</li>
|
||||
<li>Installe l'extension de navigateur et l'application mobile.</li>
|
||||
<li>Au fil de tes connexions, laisse Bitwarden enregistrer puis remplacer tes vieux mots de passe par des mots de passe générés.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: "Brave"
|
||||
slug: "brave"
|
||||
category: "navigateur"
|
||||
difficulty: "beginner"
|
||||
license: "MPL-2.0"
|
||||
url: "https://brave.com"
|
||||
color: "#fb542b"
|
||||
monogram: "Bv"
|
||||
iconSlug: "brave"
|
||||
tags: ["Bloque tout par défaut","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un navigateur basé sur Chromium qui bloque publicités et traqueurs par défaut — avec en prime une fenêtre privée connectée à Tor.
|
||||
|
||||
## why
|
||||
|
||||
C'est le changement le plus indolore quand tu viens de Chrome : même ergonomie, sans le pistage. Il bloque les pubs YouTube nativement, propose une visio chiffrée (Brave Talk, basé sur Jitsi) et un accès Tor en un clic — pratique pour récupérer un site bloqué.
|
||||
|
||||
## who
|
||||
|
||||
Celles et ceux qui veulent quitter Chrome sans rien réapprendre. (Les fonctions crypto et pubs récompensées ? Ignore-les, elles se désactivent.)
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installe Brave depuis <b>brave.com</b>.</li>
|
||||
<li>Règle les « Boucliers » (Shields) sur « agressif », et désactive Brave Rewards si tu ne veux pas de pubs.</li>
|
||||
<li>Pour Tor : menu > « Nouvelle fenêtre privée avec Tor », puis attends « Tor connecté ».</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Briar"
|
||||
slug: "briar"
|
||||
category: "messagerie"
|
||||
difficulty: "advanced"
|
||||
license: "GPL-3.0"
|
||||
url: "https://briarproject.org"
|
||||
color: "#c1272d"
|
||||
monogram: "Br"
|
||||
tags: ["Hors-ligne / P2P","Android · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie pair-à-pair qui fonctionne <b>sans internet</b> : par Bluetooth, Wi-Fi Direct ou
|
||||
Tor.
|
||||
|
||||
## why
|
||||
|
||||
Aucun serveur, donc rien à couper ni à saisir. Résiste aux coupures d'internet et à la censure
|
||||
réseau.
|
||||
|
||||
## who
|
||||
|
||||
Manifestations, catastrophes, zones isolées, blackout. Android uniquement, et les deux personnes
|
||||
doivent avoir l'application.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez depuis <b>briarproject.org</b> ou F-Droid (pas d'iOS).
|
||||
</li>
|
||||
<li>
|
||||
Créez un compte <b>local</b> (pseudo + mot de passe), stocké seulement sur le téléphone.
|
||||
</li>
|
||||
<li>Ajoutez un contact proche en scannant son QR code, ou à distance via un lien.</li>
|
||||
<li>
|
||||
Quand le réseau est coupé, activez le Bluetooth : les messages passent d'appareil à appareil.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "Duck.ai · Lumo (Proton)"
|
||||
slug: "cloudai"
|
||||
category: "ia"
|
||||
difficulty: "beginner"
|
||||
url: "https://duck.ai"
|
||||
color: "#de5833"
|
||||
monogram: "☁"
|
||||
iconSlug: "duckduckgo"
|
||||
tags: ["Cloud orienté vie privée"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des accès à l'IA dans le cloud, mais conçus pour la vie privée, quand une IA locale n'est pas
|
||||
possible.
|
||||
|
||||
## why
|
||||
|
||||
<b>Duck.ai</b> (DuckDuckGo) donne un accès anonymisé à plusieurs modèles. <b>Lumo</b> (Proton)
|
||||
chiffre les échanges et ne s'en sert pas pour entraîner. Bien mieux que ChatGPT grand public,
|
||||
mais cela reste un serveur distant.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent la commodité du cloud sans nourrir les géants. Gardez-y les sujets non
|
||||
sensibles.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: "Cloudflare 1.1.1.1 (à nuancer)"
|
||||
slug: "cloudflare"
|
||||
category: "dns"
|
||||
difficulty: "beginner"
|
||||
url: "https://one.one.one.one"
|
||||
color: "#f38020"
|
||||
monogram: "CF"
|
||||
iconSlug: "cloudflare"
|
||||
tags: ["Rapide mais centralisé","🇺🇸"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le DNS chiffré 1.1.1.1 et l'application WARP de Cloudflare : rapides, gratuits, et un vrai
|
||||
progrès face au résolveur de votre FAI.
|
||||
|
||||
## why
|
||||
|
||||
C'est utile, avec un engagement de non-journalisation audité. Mais soyez lucide : Cloudflare se
|
||||
trouve déjà devant une immense partie du web (comme intermédiaire technique, il peut voir le
|
||||
trafic en clair vers ces sites). Y router aussi votre DNS revient à{' '}
|
||||
<b>concentrer encore plus de vos traces</b> chez une seule entreprise américaine soumise au
|
||||
droit américain.
|
||||
|
||||
## who
|
||||
|
||||
Bon pour un usage courant et le blocage des maliciels (1.1.1.1 for Families). À éviter ou à
|
||||
diversifier pour un usage sensible : préférez alors Quad9 ou Mullvad DNS.
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Cryptomator"
|
||||
slug: "cryptomator"
|
||||
category: "stockage"
|
||||
difficulty: "intermediate"
|
||||
license: "GPL-3.0"
|
||||
url: "https://cryptomator.org"
|
||||
color: "#26a69a"
|
||||
monogram: "Cy"
|
||||
iconSlug: "cryptomator"
|
||||
tags: ["Chiffre avant l'envoi","🇩🇪 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un coffre qui chiffre vos fichiers <b>avant</b> de les envoyer sur n'importe quel cloud, même
|
||||
Google Drive ou Dropbox.
|
||||
|
||||
## why
|
||||
|
||||
L'astuce parfaite si vous devez garder un cloud existant : vous conservez le service pratique,
|
||||
mais le fournisseur ne voit plus que du charabia chiffré. Vous gardez seul la clé.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui ne peuvent pas quitter un cloud grand public tout de suite, mais veulent protéger leurs
|
||||
fichiers dès maintenant.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez Cryptomator (PC et mobile) depuis cryptomator.org.</li>
|
||||
<li>
|
||||
Créez un « coffre » dans le dossier synchronisé de votre cloud, choisissez un mot de passe
|
||||
fort.
|
||||
</li>
|
||||
<li>
|
||||
Placez vos fichiers dans le coffre : ils sont chiffrés puis synchronisés automatiquement.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Effacer ses données (RGPD)"
|
||||
slug: "databrokers"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "intermediate"
|
||||
url: "https://www.cnil.fr"
|
||||
color: "#b8860b"
|
||||
monogram: "RG"
|
||||
tags: ["Courtiers en données"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Faire retirer vos informations personnelles des « courtiers en données » qui les collectent et
|
||||
les revendent.
|
||||
|
||||
## why
|
||||
|
||||
En Europe, le RGPD (article 17) vous donne un <b>droit à l'effacement</b> : vous pouvez exiger,
|
||||
gratuitement, qu'une entreprise supprime vos données, en principe sous un mois. Des services
|
||||
comme <b>Incogni</b> automatisent ces demandes en masse, moyennant paiement.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent réduire leur exposition. Attention : les données réapparaissent avec le temps,
|
||||
et cela ne touche ni les registres publics ni les réseaux sociaux. Le faire soi-même est gratuit
|
||||
mais fastidieux.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Pour une demande manuelle : écrivez au courtier en invoquant l'article 17 du RGPD et demandez
|
||||
la suppression.
|
||||
</li>
|
||||
<li>
|
||||
Pour automatiser : souscrivez à un service de retrait et laissez-le envoyer les demandes en
|
||||
votre nom.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Matrix / Element"
|
||||
slug: "element"
|
||||
category: "social"
|
||||
difficulty: "intermediate"
|
||||
url: "https://element.io"
|
||||
color: "#0dbd8b"
|
||||
monogram: "El"
|
||||
iconSlug: "element"
|
||||
tags: ["Remplace Discord / Slack","🌍 · fédéré · E2EE"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie de communautés, dans l'esprit de Discord ou Slack, mais fédérée et chiffrable de
|
||||
bout en bout. Element est l'application, Matrix le réseau.
|
||||
|
||||
## why
|
||||
|
||||
Vous pouvez héberger votre propre serveur (Synapse) et contrôler vos données. Parfait pour un
|
||||
collectif, une rédaction, une association qui veut son espace indépendant.
|
||||
|
||||
## who
|
||||
|
||||
Communautés et groupes. Voir la nuance sur les métadonnées ci-dessous.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez Element (element.io) et créez un compte sur un serveur public, ou sur le vôtre
|
||||
(section 10).
|
||||
</li>
|
||||
<li>
|
||||
Rejoignez ou créez des salons, et activez le chiffrement de bout en bout pour les
|
||||
conversations privées.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Pixelfed · PeerTube · Lemmy"
|
||||
slug: "fediverse"
|
||||
category: "social"
|
||||
difficulty: "intermediate"
|
||||
url: "https://pixelfed.org"
|
||||
color: "#ea580c"
|
||||
monogram: "Pi"
|
||||
iconSlug: "pixelfed"
|
||||
tags: ["Insta · YouTube · Reddit","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Les cousins fédérés d'Instagram (Pixelfed), de YouTube (PeerTube) et de Reddit (Lemmy).
|
||||
|
||||
## why
|
||||
|
||||
Même logique que Mastodon : pas de propriétaire unique, pas d'algorithme publicitaire, pas de
|
||||
traçage. Vous publiez photos, vidéos ou discussions sans nourrir une machine à profiler.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent quitter Instagram, YouTube ou Reddit sans renoncer au format.
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: "Firefox (durci)"
|
||||
slug: "firefox"
|
||||
category: "navigateur"
|
||||
difficulty: "beginner"
|
||||
license: "MPL-2.0"
|
||||
url: "https://www.mozilla.org/firefox"
|
||||
color: "#e66000"
|
||||
monogram: "Fx"
|
||||
iconSlug: "firefoxbrowser"
|
||||
tags: ["Remplace Chrome","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le navigateur libre de référence pour un usage quotidien respectueux de la vie privée.
|
||||
|
||||
## why
|
||||
|
||||
Contrairement à Chrome (un mouchard publicitaire de Google), Firefox est développé par une
|
||||
fondation et n'a pas d'intérêt à vous profiler. Avec quelques réglages (le guide{' '}
|
||||
<em>arkenfox</em>) et l'extension uBlock Origin, il devient très solide.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, comme navigateur principal au quotidien.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez Firefox depuis <b>mozilla.org/firefox</b>.
|
||||
</li>
|
||||
<li>
|
||||
Ajoutez l'extension <b>uBlock Origin</b> (blocage des pubs et traqueurs).
|
||||
</li>
|
||||
<li>
|
||||
Dans les réglages de vie privée, choisissez « Stricte », désactivez la télémétrie, et videz
|
||||
les cookies à la fermeture. Pour aller plus loin, appliquez le guide arkenfox.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Bitwarden"
|
||||
slug: "bitwarden"
|
||||
lang: "gcf"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
On kòf a modpas ki ka sonjé-yo, jénéré-yo é ranpli-yo pou vou, asi tout apawèy a-w.
|
||||
|
||||
## why
|
||||
|
||||
Gratis, odité, miltiplatfòwm, chifré bout-an-bout. Ou ni yenki on sèl modpas fò pou sonjé — é jou ou vlé pa dépann di pèsonn, ou pé <b>oto-ébèjé'y</b> (èvè Vaultwarden).
|
||||
|
||||
## who
|
||||
|
||||
Tout moun. Sé fondasyon a tout sékirité a-w : on modpas inik é fò pou chak sèwvis, san fòsé mémwa a-w.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Kréyé on kont asi bitwarden.com é chwazi on modpas mèt <b>long</b> — on fraz antyé. Pa janmé oublié'y : i pa rékipérab, é sé fè èsprè.</li>
|
||||
<li>Enstalé èkstansyon navigatè-la é aplikasyon mobil-la.</li>
|
||||
<li>O fi a konèksyon a-w, kité Bitwarden anrèjistré é apré ranplasé vyé modpas a-w pa modpas jénéré.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Brave"
|
||||
slug: "brave"
|
||||
lang: "gcf"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
On navigatè basé asi Chromium ki ka bloké piblisité é trakè pa défo — èvè an plis on fenèt privé konèkté asi Tor.
|
||||
|
||||
## why
|
||||
|
||||
Sé chanjman pi dous-la lè ou ka sòti an Chrome : menm èwgonomi, san pistaj-la. I ka bloké pib YouTube dirèk, i ka pwopozé on vizyo chifré (Brave Talk, basé asi Jitsi) é on aksè Tor an on klik — pratik pou ratrapé on sit bloké.
|
||||
|
||||
## who
|
||||
|
||||
Moun ki vlé kité Chrome san riaprann ayen. (Fonksyon crypto é pib rékonpansé ? Inyoré-yo, yo ka tenn.)
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Enstalé Brave dépi <b>brave.com</b>.</li>
|
||||
<li>Réglé « Bouklyé »-la (Shields) asi « agrésif », é tenn Brave Rewards si ou pa vlé pib.</li>
|
||||
<li>Pou Tor : mèni > « Nouvo fenèt privé èvè Tor », apré atann « Tor konèkté ».</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: "Proton Mail"
|
||||
slug: "protonmail"
|
||||
lang: "gcf"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
On bwèt imèl chifré ki ka ranplasé Gmail oben Outlook — é kont pivo a tout ékosistèm Proton-la.
|
||||
|
||||
## why
|
||||
|
||||
Basé an Swis, andéwò a jiridiksyon « Five Eyes »-la. Konpatib OpenPGP : chifraj-la otomatik ant kont Proton é lòt sèwvis PGP. Kouryé a-w, sé korèspondans a-w — pa on matyè prèmyé pou piblisité.
|
||||
|
||||
## who
|
||||
|
||||
Tout moun. Sé pi bon pòt pou sòti an Google : louvè kont-la, rès a suit-la (ajanda, drive, VPN) ka suiv tousèl.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Kréyé on kont asi <b>proton.me</b> (òf gratis-la ka sifi pou koumansé).</li>
|
||||
<li>Rapatriyé vyé mésaj é kontak a-w èvè <b>Easy Switch</b> (entégré).</li>
|
||||
<li>Mèt on rédirèksyon dépi Gmail é prévien kontak a-w di nouvo adrès a-w.</li>
|
||||
<li>Enstalé aplikasyon mobil-la, limé <b>doub otantifikasyon-la</b> — é Proton Bridge si ou ka rété asi Thunderbird.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "Quad9"
|
||||
slug: "quad9"
|
||||
lang: "gcf"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
On rézolvè DNS chifré, swis é san bi likratif, ki ka bloké an plis domèn malfézan yo ja konnèt.
|
||||
|
||||
## why
|
||||
|
||||
Founisè aksè a-w ka vwè chak sit ou ka vizité gras a DNS-la. Quad9 ka chifré dimann-lasa yo, i pa ka gadé adrès IP a-w, é i ka pwotéjé-w kont lamèkantri an menm tan. On réglaj « fè'y é oublié'y ».
|
||||
|
||||
## who
|
||||
|
||||
Tout moun — sitou si ou vlé on vré gany san enstalé ayen anplis.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Asi Android : Réglaj > Rézo > <b>DNS privé</b> > matjé <b>dns.quad9.net</b>.</li>
|
||||
<li>Asi iPhone/Mac : enstalé profil DoH-la dépi quad9.net. Asi PC : konfiguré DNS-over-HTTPS an navigatè-la oben an sistèm-la.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: "Signal"
|
||||
slug: "signal"
|
||||
lang: "gcf"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
On mésajri chifré pou tèks, apèl, vizyo é group a-w. Sé ranplasan dirèk a WhatsApp — menm senplisité, san Meta dèyè.
|
||||
|
||||
## why
|
||||
|
||||
Chifraj bout-an-bout pa défo, san bi likratif, protokòl odité ki ka sèwvi référans toupatou. Prèské pon métadoné pa ka rété, é ékip-la ja di : yo ké kité Léwòp olyé yo enstalé on skannaj. Chak moun ou ka fè pasé, sé on konvèsasyon anplis ki ka sòti an survéyans-la.
|
||||
|
||||
## who
|
||||
|
||||
Tout moun, jòdi-la menm. Sé prèmyé jès-la, é pi itil-la : koumansé èvè group a fanmi a-w. Yenki on kontrent : on niméwo téléfòn — ou pé séré'y dèyè on non itilizatè.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Enstalé dépi <b>signal.org</b> (App Store, Google Play, oben APK dirèk asi Android).</li>
|
||||
<li>Konfirmé niméwo a-w pa SMS, apré chwazi on kòd PIN.</li>
|
||||
<li>Réglaj > Konfidansyalité : kréyé on <b>non itilizatè</b> pou séré niméwo a-w, é limé <b>vèwou enskripsyon-la</b>.</li>
|
||||
<li>Envité moun a-w — group fanmi-la avan. Asi òwdinatè, lyanné aplikasyon biwo-la èvè téléfòn a-w.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: "uBlock Origin"
|
||||
slug: "ublock"
|
||||
lang: "gcf"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Èkstansyon-la ki ka bloké piblisité, trakè é èskrip èspyon. Jès pi rantab a tout gid-lasa : dis ségonn, éfè imedya.
|
||||
|
||||
## why
|
||||
|
||||
Piblisité an liny sé prensipal chimen a survéyans é profilaj. Bloké'y, sé koupé pi gwo moso a pistaj-la — é an menm balan paj a-w ka chajé pi vit é fòfé data a-w ka respiré.
|
||||
|
||||
## who
|
||||
|
||||
Tout moun, asi Firefox kon asi navigatè Chromium. Si ou ka fè yenki on sèl jès jòdi-la, fè ta-lasa.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Louvè magazen èkstansyon a navigatè a-w é chèché « uBlock Origin » (orijinal-la, pa Raymond Hill).</li>
|
||||
<li>Enstalé'y. I ka travay lamenm, san pon réglaj.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
name: "GrapheneOS"
|
||||
slug: "grapheneos"
|
||||
category: "os"
|
||||
difficulty: "advanced"
|
||||
license: "MIT"
|
||||
url: "https://grapheneos.org"
|
||||
color: "#1f8a70"
|
||||
monogram: "Gr"
|
||||
iconSlug: "grapheneos"
|
||||
tags: ["Mobile dégooglisé","Android · Pixel · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un Android entièrement dégooglisé, installé sur un téléphone Pixel : la souveraineté sur mobile.
|
||||
|
||||
## why
|
||||
|
||||
C'est la défense structurelle par excellence contre un OS ou un magasin d'applications qui
|
||||
voudrait vous scanner. Services Google en bac à sable optionnel,{' '}
|
||||
<b>permissions réseau par application</b>, profils cloisonnés, redémarrage automatique qui purge
|
||||
les clés de la mémoire, code PIN de contrainte qui efface le téléphone. Réputé pour sa sécurité
|
||||
au point d'être utilisé par des cibles de logiciels espions.
|
||||
|
||||
## who
|
||||
|
||||
Profils exposés, mais aussi tout citoyen prêt à acheter un Pixel pour reprendre le contrôle de
|
||||
son téléphone.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Procurez-vous un téléphone <b>Google Pixel</b> compatible (c'est le seul matériel supporté,
|
||||
pour des raisons de sécurité).
|
||||
</li>
|
||||
<li>
|
||||
Sur un ordinateur, allez sur <b>grapheneos.org/install</b> et suivez l'installeur web
|
||||
(quelques clics, aucun terminal requis).
|
||||
</li>
|
||||
<li>
|
||||
Pour vos applications, installez le magasin libre F-Droid ; ajoutez Google Play en bac à sable
|
||||
seulement si nécessaire.
|
||||
</li>
|
||||
<li>
|
||||
Réglez un long mot de passe (pas un code à 4 chiffres) et découvrez le mode « Lockdown ».
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "IVPN"
|
||||
slug: "ivpn"
|
||||
category: "vpn"
|
||||
difficulty: "intermediate"
|
||||
url: "https://www.ivpn.net"
|
||||
color: "#3b5bdb"
|
||||
monogram: "iv"
|
||||
tags: ["Sans e-mail · no-log","🇬🇮 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un petit fournisseur de VPN particulièrement rigoureux et transparent, sur le même modèle que
|
||||
Mullvad.
|
||||
|
||||
## why
|
||||
|
||||
Politique no-log auditée, pas d'e-mail obligatoire, paiement en crypto ou en espèces. Il propose
|
||||
même des options anti-traçage (blocage des trackers au niveau réseau) et une inscription sans
|
||||
aucun identifiant.
|
||||
|
||||
## who
|
||||
|
||||
Une alternative solide à Mullvad, pour les profils pseudonymes et à haut risque qui veulent
|
||||
diversifier.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Sur <b>ivpn.net</b>, générez un compte (un identifiant est créé pour vous, sans e-mail).
|
||||
</li>
|
||||
<li>Payez en cryptomonnaie ou en espèces, comme pour Mullvad.</li>
|
||||
<li>
|
||||
Installez l'application, connectez-vous hors UE, activez le kill switch et le protocole
|
||||
WireGuard.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "KeePassXC"
|
||||
slug: "keepassxc"
|
||||
category: "motsdepasse"
|
||||
difficulty: "advanced"
|
||||
license: "GPL-2.0/3.0"
|
||||
url: "https://keepassxc.org"
|
||||
color: "#2c6b9e"
|
||||
monogram: "Kp"
|
||||
iconSlug: "keepassxc"
|
||||
tags: ["100% local","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un coffre-fort <b>local</b> : un fichier chiffré que vous seul détenez, sans aucun cloud ni
|
||||
compte.
|
||||
|
||||
## why
|
||||
|
||||
Rien ne quitte votre appareil, sauf si vous décidez de synchroniser le fichier vous-même (par
|
||||
exemple via Cryptomator). C'est le choix des puristes qui ne veulent aucun intermédiaire.
|
||||
|
||||
## who
|
||||
|
||||
Les profils avancés qui veulent le contrôle total et zéro dépendance en ligne.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez KeePassXC (PC) et une application compatible sur mobile (KeePassDX sur Android,
|
||||
Strongbox sur iOS).
|
||||
</li>
|
||||
<li>Créez un fichier de base de données protégé par un mot de passe maître.</li>
|
||||
<li>Pour l'avoir sur tous vos appareils, synchronisez ce fichier via un cloud chiffré.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: "Linux (sur ordinateur)"
|
||||
slug: "linux"
|
||||
category: "os"
|
||||
difficulty: "intermediate"
|
||||
url: "https://distrochooser.de/fr"
|
||||
color: "#2b2b2b"
|
||||
monogram: "🐧"
|
||||
iconSlug: "linux"
|
||||
tags: ["Remplace Windows / macOS","🌍 · libre & gratuit"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un système d'exploitation libre et gratuit pour votre ordinateur, en remplacement de Windows ou
|
||||
de macOS. Il en existe de nombreuses variantes, appelées « distributions ».
|
||||
|
||||
## why
|
||||
|
||||
C'est le point aveugle de beaucoup de gens : ils chiffrent leurs messages mais gardent un
|
||||
Windows qui transmet en continu de la télémétrie à Microsoft (et dont la fonction « Recall » a
|
||||
voulu photographier l'écran en permanence), ou un macOS qui signale les applications que vous
|
||||
lancez. Linux, lui, ne vous espionne pas, il est gratuit, il ressuscite les vieux ordinateurs et
|
||||
il vous rend enfin maître de votre machine. C'est plus facile que sa réputation : les
|
||||
distributions modernes ressemblent à Windows ou macOS.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde peut franchir le pas. Pour débuter, <b>Linux Mint</b> (le plus proche de Windows)
|
||||
ou <b>Fedora</b> ; <b>Debian</b> pour la stabilité. Pas besoin de tout casser : on peut d'abord
|
||||
tester, puis installer à côté de son système actuel.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Pas sûr de la distribution ? Répondez au questionnaire <b>distrochooser.de/fr</b>, qui vous
|
||||
oriente selon vos besoins.
|
||||
</li>
|
||||
<li>
|
||||
Testez-la <b>sans rien installer</b> directement dans votre navigateur sur{' '}
|
||||
<b>distrosea.com</b>.
|
||||
</li>
|
||||
<li>
|
||||
Une fois convaincu, téléchargez l'image de la distribution et créez une clé USB de démarrage
|
||||
(avec Ventoy ou Balena Etcher).
|
||||
</li>
|
||||
<li>
|
||||
Démarrez sur la clé pour essayer le système « en live », puis lancez l'installation. Activez
|
||||
le chiffrement du disque proposé.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: "Ollama · LM Studio · Jan · GPT4All"
|
||||
slug: "localai"
|
||||
category: "ia"
|
||||
difficulty: "intermediate"
|
||||
url: "https://lmstudio.ai"
|
||||
color: "#111111"
|
||||
monogram: "Ai"
|
||||
iconSlug: "ollama"
|
||||
tags: ["Remplace ChatGPT","🌍 · 100% local"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des logiciels pour faire tourner un modèle d'intelligence artificielle directement sur votre
|
||||
ordinateur.
|
||||
|
||||
## why
|
||||
|
||||
Vos questions ne quittent jamais l'appareil : aucun serveur, aucun compte, aucune télémétrie, et
|
||||
ça marche hors ligne. La qualité dépend de votre matériel, mais rien ne fuit. <b>Ollama</b> et{' '}
|
||||
<b>LM Studio</b> sont les plus simples ; <b>Jan</b> et <b>GPT4All</b> visent la vie privée
|
||||
maximale.
|
||||
|
||||
## who
|
||||
|
||||
Tous ceux qui utilisent l'IA pour des sujets un tant soit peu personnels et ne veulent pas les
|
||||
confier à un serveur américain.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Pour débuter en douceur, installez <b>LM Studio</b> ou <b>Jan</b> (interface graphique, comme
|
||||
une application classique).
|
||||
</li>
|
||||
<li>
|
||||
Téléchargez un modèle proposé dans l'application (par exemple Llama ou Mistral), adapté à la
|
||||
puissance de votre machine.
|
||||
</li>
|
||||
<li>
|
||||
Discutez : tout se passe en local. Ollama est la version en ligne de commande pour les plus
|
||||
techniques.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: "mailbox.org · Posteo"
|
||||
slug: "mailbox"
|
||||
category: "email"
|
||||
difficulty: "beginner"
|
||||
url: "https://mailbox.org"
|
||||
color: "#2470d4"
|
||||
monogram: "mx"
|
||||
tags: ["Mail classique éthique","🇩🇪 · UE · PGP"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Deux fournisseurs de mail européens, éthiques et peu coûteux, avec support PGP côté serveur.
|
||||
|
||||
## why
|
||||
|
||||
Respectueux de la vie privée et de l'écologie, sur des standards ouverts (IMAP, PGP). Ils
|
||||
fonctionnent avec n'importe quel logiciel de messagerie.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent un mail « classique » (relevé dans Thunderbird) mais éthique, plutôt qu'une
|
||||
messagerie chiffrée fermée.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Créez un compte (payant, de l'ordre de 1 à 3 € par mois).</li>
|
||||
<li>Activez le chiffrement PGP côté serveur (fonction « Guard » chez mailbox.org).</li>
|
||||
<li>Configurez le compte dans Thunderbird ou l'application mobile de votre choix.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: "Organic Maps · OsmAnd"
|
||||
slug: "maps"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "beginner"
|
||||
url: "https://organicmaps.app"
|
||||
color: "#0a7d33"
|
||||
monogram: "Om"
|
||||
iconSlug: "openstreetmap"
|
||||
tags: ["Remplace Google Maps","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des applications de cartes et de navigation, fondées sur OpenStreetMap, qui fonctionnent hors
|
||||
ligne.
|
||||
|
||||
## why
|
||||
|
||||
Google Maps enregistre chacun de vos trajets et bâtit un historique de lieux d'une précision
|
||||
redoutable. <b>Organic Maps</b> ne trace rien et n'affiche aucune publicité ; <b>OsmAnd</b> vise
|
||||
les utilisateurs avancés (randonnée, courbes de niveau, navigation détaillée).
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. Le trafic en temps réel et les transports sont moins complets que chez Google,
|
||||
mais la navigation quotidienne est excellente.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez depuis F-Droid, l'App Store ou Google Play.</li>
|
||||
<li>
|
||||
Téléchargez à l'avance les cartes des régions qui vous intéressent pour l'usage hors ligne.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Mastodon"
|
||||
slug: "mastodon"
|
||||
category: "social"
|
||||
difficulty: "beginner"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://joinmastodon.org"
|
||||
color: "#6364ff"
|
||||
monogram: "Ma"
|
||||
iconSlug: "mastodon"
|
||||
tags: ["Remplace X / Twitter","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
L'alternative à X/Twitter : un réseau social fait de milliers de serveurs indépendants qui
|
||||
communiquent entre eux (le « fédiverse »).
|
||||
|
||||
## why
|
||||
|
||||
Pas de patron unique, pas d'algorithme qui décide à votre place, pas de bannissement arbitraire
|
||||
par une seule entreprise. Le fil est chronologique, sans publicité ni manipulation.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. Idéal pour les comptes d'information qui veulent une présence qu'aucune
|
||||
entreprise ne peut couper.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Sur joinmastodon.org, choisissez un serveur (ou « instance ») qui vous correspond, puis créez
|
||||
un compte.
|
||||
</li>
|
||||
<li>
|
||||
Installez une application (l'officielle, ou Ivory, Tusky…) et suivez des comptes : vous voyez
|
||||
tout le fédiverse, quel que soit leur serveur.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Molly"
|
||||
slug: "molly"
|
||||
category: "messagerie"
|
||||
difficulty: "advanced"
|
||||
license: "GPL-3.0"
|
||||
url: "https://molly.im"
|
||||
color: "#6d4bb3"
|
||||
monogram: "M"
|
||||
tags: ["Signal durci","Android · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une version durcie de Signal pour Android, compatible avec le même réseau.
|
||||
|
||||
## why
|
||||
|
||||
Base de données chiffrée au repos, verrouillage automatique, et une variante « FOSS »
|
||||
entièrement sans services Google.
|
||||
|
||||
## who
|
||||
|
||||
Utilisateurs avancés, notamment sur GrapheneOS, avec un modèle de menace élevé. Android
|
||||
uniquement.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Ajoutez le dépôt Molly dans F-Droid, ou récupérez l'APK sur <b>molly.im</b>.
|
||||
</li>
|
||||
<li>
|
||||
Choisissez <b>Molly-FOSS</b> si vous voulez zéro dépendance Google.
|
||||
</li>
|
||||
<li>Enregistrez-vous avec votre numéro (même réseau que Signal).</li>
|
||||
<li>Définissez un mot de passe de base de données et le verrouillage automatique.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Monero (XMR)"
|
||||
slug: "monero"
|
||||
category: "argent"
|
||||
difficulty: "advanced"
|
||||
license: "BSD-3-Clause"
|
||||
url: "https://www.getmonero.org"
|
||||
color: "#ff6600"
|
||||
monogram: "ɱ"
|
||||
iconSlug: "monero"
|
||||
tags: ["Confidentiel par défaut"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
La cryptomonnaie de la vie privée : montants, expéditeur et destinataire sont masqués par
|
||||
défaut.
|
||||
|
||||
## why
|
||||
|
||||
C'est l'exact opposé de Bitcoin sur ce point : les transactions sont confidentielles par
|
||||
conception. C'est le meilleur moyen de payer un service sans laisser de trace reliable à vous.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent le maximum de confidentialité financière. Note : Monero est déjà retiré de
|
||||
plusieurs plateformes en Europe, il faut souvent passer par un échange décentralisé.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez un portefeuille (l'officiel sur getmonero.org, ou Cake Wallet sur mobile).</li>
|
||||
<li>
|
||||
Obtenez des XMR via un service d'échange décentralisé (par exemple un « swap » depuis une
|
||||
autre crypto).
|
||||
</li>
|
||||
<li>Sauvegardez votre phrase de récupération sur papier.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: "Mullvad VPN"
|
||||
slug: "mullvad"
|
||||
category: "vpn"
|
||||
difficulty: "beginner"
|
||||
url: "https://mullvad.net"
|
||||
color: "#2b4b8f"
|
||||
monogram: "Mv"
|
||||
iconSlug: "mullvad"
|
||||
tags: ["Sans compte · cash/Monero","🇸🇪 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un VPN qui masque votre adresse IP et chiffre votre trafic jusqu'à ses serveurs, sans jamais
|
||||
vous demander la moindre information personnelle.
|
||||
|
||||
## why
|
||||
|
||||
C'est l'étalon-or de la vie privée. Pas d'e-mail, pas de nom : à l'inscription, on vous attribue
|
||||
un simple numéro de compte. Politique « no-log » auditée, prix unique et honnête (5 € par mois,
|
||||
sans palier ni fausse promesse), et paiement possible en espèces ou en Monero. C'est précisément
|
||||
ce qui compte le jour où un État tente de couper l'accès aux VPN : un compte impossible à relier
|
||||
à votre identité, et un paiement que les banques ne peuvent pas bloquer.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, du citoyen qui veut contourner une vérification d'identité au profil à haut
|
||||
risque. C'est le VPN à recommander en priorité.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Allez sur <b>mullvad.net</b>. Si le site est bloqué dans votre pays, ouvrez-le via le
|
||||
navigateur Tor ou son adresse <b>.onion</b>. Cliquez sur « Générer un numéro de compte » :
|
||||
aucun e-mail ni nom n'est demandé. Notez ce numéro dans votre gestionnaire de mots de passe et
|
||||
ne le partagez jamais, c'est votre unique clé d'accès.
|
||||
</li>
|
||||
<li>
|
||||
Choisissez la durée et le mode de paiement. Le prix est fixe. Trois voies : carte bancaire (le
|
||||
plus simple si elle n'est pas bloquée), espèces envoyées par courrier, ou cryptomonnaie.
|
||||
</li>
|
||||
<li>
|
||||
<b>Paiement anonyme en crypto :</b> cliquez sur « Créer une adresse de paiement à usage unique
|
||||
». Mullvad affiche une adresse (Bitcoin ou Monero) et un montant exact. Envoyez ce montant
|
||||
depuis votre portefeuille. Pour l'anonymat maximal, préférez <b>Monero</b> (confidentiel par
|
||||
défaut) ; Bitcoin est plus simple mais traçable. Voir la section 09 pour acheter de la crypto.
|
||||
Comptez environ 30 minutes de confirmation.
|
||||
</li>
|
||||
<li>
|
||||
Téléchargez l'application sur <b>mullvad.net/download</b> (via Tor si le site est filtré) et
|
||||
installez-la. Sur mobile, passez par l'App Store, Google Play, ou l'APK officiel si
|
||||
l'application a été retirée de votre magasin.
|
||||
</li>
|
||||
<li>
|
||||
Lancez l'application, entrez votre numéro de compte, puis connectez-vous à un serveur situé{' '}
|
||||
<b>hors de votre pays</b>, idéalement hors Union européenne (Suisse, États-Unis…). Activez le
|
||||
« kill switch » (qui coupe Internet si le VPN tombe) et le DNS de Mullvad.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Mullvad Browser"
|
||||
slug: "mullvadbrowser"
|
||||
category: "navigateur"
|
||||
difficulty: "intermediate"
|
||||
url: "https://mullvad.net/browser"
|
||||
color: "#2b4b8f"
|
||||
monogram: "Mb"
|
||||
iconSlug: "mullvad"
|
||||
tags: ["Anti-empreinte","🇸🇪 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le navigateur du Tor Browser, mais sans le réseau Tor : à utiliser avec votre VPN pour un
|
||||
ordinateur difficile à pister.
|
||||
|
||||
## why
|
||||
|
||||
Développé avec le Tor Project, il est conçu pour que tous ses utilisateurs présentent la{' '}
|
||||
<b>même empreinte numérique</b>. Résultat : au lieu d'être unique et donc traçable, vous vous
|
||||
fondez dans la masse. C'est le meilleur choix de vie privée sur ordinateur.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent aller au-delà du simple blocage de pub, notamment les comptes pseudonymes et
|
||||
les profils exposés.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Téléchargez-le sur <b>mullvad.net/browser</b> (Windows, macOS, Linux).
|
||||
</li>
|
||||
<li>
|
||||
Utilisez-le tel quel, sans le personnaliser : chaque extension ou réglage ajouté vous rend à
|
||||
nouveau identifiable.
|
||||
</li>
|
||||
<li>Associez-le à votre VPN pour masquer aussi votre adresse IP.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Mullvad DNS · NextDNS"
|
||||
slug: "mullvaddns"
|
||||
category: "dns"
|
||||
difficulty: "intermediate"
|
||||
url: "https://mullvad.net/help/dns-over-https-and-dns-over-tls"
|
||||
color: "#2b4b8f"
|
||||
monogram: "DNS"
|
||||
iconSlug: "mullvad"
|
||||
tags: ["Filtrage réseau"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Deux résolveurs chiffrés qui bloquent en plus pubs et traqueurs pour tout l'appareil.
|
||||
|
||||
## why
|
||||
|
||||
<b>Mullvad DNS</b> (Suède, no-log) est gratuit et propose des listes de blocage. <b>NextDNS</b>{' '}
|
||||
est très personnalisable, avec un tableau de bord et des profils par appareil, mais il est basé
|
||||
aux États-Unis et journalise par défaut (à désactiver dans les réglages).
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent un blocage des pubs à l'échelle du système, sans installer d'application par
|
||||
navigateur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Mullvad DNS : utilisez l'adresse indiquée sur mullvad.net/help/dns comme DNS privé (même
|
||||
méthode que Quad9).
|
||||
</li>
|
||||
<li>
|
||||
NextDNS : créez un profil sur nextdns.io, <b>désactivez la journalisation</b>, puis appliquez
|
||||
la configuration à vos appareils.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Nextcloud"
|
||||
slug: "nc-storage"
|
||||
category: "stockage"
|
||||
difficulty: "advanced"
|
||||
url: "https://nextcloud.com"
|
||||
color: "#0082c9"
|
||||
monogram: "Nc"
|
||||
iconSlug: "nextcloud"
|
||||
tags: ["Votre propre cloud","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Votre propre nuage (fichiers, agenda, contacts, notes) hébergé sur votre serveur.
|
||||
|
||||
## why
|
||||
|
||||
Le degré ultime de contrôle : vos données ne quittent jamais votre matériel. Détaillé dans la
|
||||
section 10 (Auto-hébergement).
|
||||
|
||||
## who
|
||||
|
||||
Les auto-hébergeurs. Voir la fiche complète en section 10.
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: "Nextcloud"
|
||||
slug: "nextcloud"
|
||||
category: "selfhost"
|
||||
difficulty: "advanced"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://nextcloud.com"
|
||||
color: "#0082c9"
|
||||
monogram: "Nc"
|
||||
iconSlug: "nextcloud"
|
||||
tags: ["Remplace toute la suite Google","🇩🇪 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Votre propre nuage : fichiers, agenda, contacts, photos, notes et documents collaboratifs, sur
|
||||
votre serveur.
|
||||
|
||||
## why
|
||||
|
||||
C'est le remplacement complet de Google Drive, Agenda et Contacts, mais chez vous. Vous décidez
|
||||
de la juridiction et personne d'autre n'a la main sur vos fichiers. À savoir : c'est la
|
||||
confidentialité « votre serveur », pas un chiffrement de bout en bout par défaut ; vous devez le
|
||||
maintenir à jour.
|
||||
|
||||
## who
|
||||
|
||||
Les auto-hébergeurs. Le plus simple est de l'installer via YunoHost ou Umbrel.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Nostr"
|
||||
slug: "nostr"
|
||||
category: "social"
|
||||
difficulty: "intermediate"
|
||||
url: "https://nostr.com"
|
||||
color: "#8e30eb"
|
||||
monogram: "No"
|
||||
tags: ["Incensurable","🌍 · protocole ouvert"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Pas une application mais un protocole : un réseau social où votre identité est une simple paire
|
||||
de clés, comme pour Bitcoin.
|
||||
|
||||
## why
|
||||
|
||||
Zéro e-mail, zéro mot de passe, aucun compte qu'on puisse supprimer. Vos messages voyagent sur
|
||||
des relais décentralisés et vous possédez vraiment votre audience : personne ne peut vous bannir
|
||||
durablement. Vous pouvez même recevoir des pourboires en Bitcoin (les « zaps ») et changer
|
||||
d'application quand vous voulez sans perdre vos abonnés.
|
||||
|
||||
## who
|
||||
|
||||
Comptes d'information et créateurs qui craignent la censure de plateforme et veulent une
|
||||
présence vraiment à eux.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installez un client : Damus (iPhone), Amethyst ou Primal (Android), ou une version web.</li>
|
||||
<li>
|
||||
L'application génère votre paire de clés. <b>Sauvegardez votre clé privée (nsec)</b> comme une
|
||||
phrase de récupération Bitcoin : sur papier, jamais partagée.
|
||||
</li>
|
||||
<li>Créez votre profil, suivez des gens, activez les zaps pour recevoir des sats.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
name: "Standard Notes · Joplin · CryptPad"
|
||||
slug: "notes"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "beginner"
|
||||
url: "https://standardnotes.com"
|
||||
color: "#2e7d6b"
|
||||
monogram: "No"
|
||||
tags: ["Remplace Docs / Notion","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des notes et documents chiffrés de bout en bout, pour remplacer Google Docs, Notion ou Evernote.
|
||||
|
||||
## why
|
||||
|
||||
<b>Standard Notes</b> (par Proton) chiffre vos notes par défaut. <b>Joplin</b> gère des carnets
|
||||
en Markdown avec chiffrement optionnel et la synchronisation de votre choix. <b>CryptPad</b>{' '}
|
||||
(français) offre l'équivalent de Google Docs en temps réel, entièrement chiffré.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde pour les notes ; CryptPad pour la collaboration à plusieurs.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Standard Notes / Joplin : créez un compte, installez l'application, et activez le chiffrement
|
||||
(par défaut sur Standard Notes, à activer dans Joplin).
|
||||
</li>
|
||||
<li>
|
||||
CryptPad : utilisez cryptpad.fr ou une autre instance, aucun compte requis pour commencer.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "Ente · Immich"
|
||||
slug: "photos"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "intermediate"
|
||||
url: "https://ente.io"
|
||||
color: "#7b3fbf"
|
||||
monogram: "Ph"
|
||||
iconSlug: "immich"
|
||||
tags: ["Remplace Google Photos","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Deux façons de garder vos photos sans les confier à Google ou Apple.
|
||||
|
||||
## why
|
||||
|
||||
<b>Ente</b> est un service géré, chiffré de bout en bout, avec reconnaissance et recherche
|
||||
faites sur l'appareil : simple et sûr. <b>Immich</b> est un clone de Google Photos que vous{' '}
|
||||
<b>hébergez vous-même</b> (recherche par visages et objets incluse), à réserver à ceux qui
|
||||
savent gérer un serveur.
|
||||
|
||||
## who
|
||||
|
||||
Ente pour tous ; Immich pour les auto-hébergeurs. Attention : Immich n'est pas chiffré de bout
|
||||
en bout, sa sécurité dépend de votre serveur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Ente : installez l'application, créez un compte, activez la sauvegarde automatique de votre
|
||||
pellicule.
|
||||
</li>
|
||||
<li>
|
||||
Immich : déployez-le sur votre serveur (voir section 10) via Docker, puis connectez
|
||||
l'application mobile.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "Pi-hole · AdGuard Home"
|
||||
slug: "pihole"
|
||||
category: "dns"
|
||||
difficulty: "advanced"
|
||||
url: "https://pi-hole.net"
|
||||
color: "#96060c"
|
||||
monogram: "π"
|
||||
iconSlug: "pihole"
|
||||
tags: ["Blocage tout le réseau","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un bloqueur de publicités et de traqueurs qui protège <b>tout votre réseau à la fois</b>,
|
||||
installé sur un petit ordinateur comme un Raspberry Pi.
|
||||
|
||||
## why
|
||||
|
||||
Là où uBlock Origin protège un navigateur, Pi-hole filtre <b>chaque appareil</b> de la maison, y
|
||||
compris ceux où l'on ne peut pas installer d'extension : téléphones, téléviseurs connectés,
|
||||
objets connectés. Les pubs et traqueurs sont bloqués au niveau du DNS, avant même de se charger.{' '}
|
||||
<b>AdGuard Home</b> est une alternative à l'interface plus moderne.
|
||||
|
||||
## who
|
||||
|
||||
Les foyers qui veulent nettoyer tous leurs appareils d'un coup, et qui ont (ou veulent) un
|
||||
Raspberry Pi ou un petit serveur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Sur un Raspberry Pi (ou un conteneur), lancez l'installeur officiel de Pi-hole.</li>
|
||||
<li>
|
||||
Dans les réglages de votre box/routeur, indiquez l'adresse du Pi-hole comme serveur DNS.
|
||||
</li>
|
||||
<li>Désormais, tous les appareils connectés à votre réseau sont filtrés automatiquement.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: "PocketPal AI"
|
||||
slug: "pocketpal"
|
||||
category: "ia"
|
||||
difficulty: "intermediate"
|
||||
license: "MIT"
|
||||
url: "https://pocketpal.dev"
|
||||
color: "#6b4eff"
|
||||
monogram: "Pp"
|
||||
tags: ["Remplace ChatGPT sur mobile","📱 · 100% local"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une application pour faire tourner des modèles d'IA directement sur votre téléphone, entièrement
|
||||
hors ligne.
|
||||
|
||||
## why
|
||||
|
||||
Vos requêtes ne quittent jamais le téléphone : aucun compte, aucun cloud, aucune télémétrie, et
|
||||
ça marche en mode avion. Open source (MIT), l'appli télécharge des modèles <b>GGUF</b> (Gemma,
|
||||
Qwen, Phi, Llama) depuis Hugging Face et les exécute sur le CPU, le GPU ou le NPU de l'appareil.
|
||||
Le pendant mobile d'Ollama et LM Studio.
|
||||
|
||||
## who
|
||||
|
||||
Tous ceux qui dégainent ChatGPT sur leur téléphone pour des questions personnelles et ne veulent
|
||||
pas les confier à un serveur américain.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez <b>PocketPal</b> depuis l'App Store ou Google Play (ou compilez les sources).
|
||||
</li>
|
||||
<li>
|
||||
Ouvrez <b>Models</b>, choisissez un modèle adapté à votre téléphone et téléchargez-le.
|
||||
</li>
|
||||
<li>Chargez-le et discutez : tout tourne hors ligne, sur l'appareil.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "Proton Drive"
|
||||
slug: "protondrive"
|
||||
category: "stockage"
|
||||
difficulty: "beginner"
|
||||
url: "https://proton.me/drive"
|
||||
color: "#6d4aff"
|
||||
monogram: "Pd"
|
||||
iconSlug: "protondrive"
|
||||
tags: ["Remplace Google Drive / iCloud","🇨🇭 · E2EE"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un cloud chiffré de bout en bout pour vos fichiers et vos photos, intégré à Proton.
|
||||
|
||||
## why
|
||||
|
||||
Contrairement à Google Drive ou iCloud, Proton ne peut pas lire vos fichiers : ils sont chiffrés
|
||||
avant de quitter votre appareil. Juridiction suisse, même compte que Proton Mail.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, comme remplacement direct et simple du cloud de Google ou d'Apple.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Activez Proton Drive dans votre compte Proton et installez l'application.</li>
|
||||
<li>Activez la sauvegarde automatique de vos photos, puis désactivez celle de Google/Apple.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: "Proton Mail"
|
||||
slug: "protonmail"
|
||||
category: "email"
|
||||
difficulty: "beginner"
|
||||
url: "https://proton.me/mail"
|
||||
color: "#6d4aff"
|
||||
monogram: "Pm"
|
||||
iconSlug: "protonmail"
|
||||
tags: ["Remplace Gmail","🇨🇭 · open-source · OpenPGP"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une boîte mail chiffrée qui remplace Gmail ou Outlook — et le compte pivot de tout l'écosystème Proton.
|
||||
|
||||
## why
|
||||
|
||||
Basée en Suisse, hors juridiction des « Five Eyes ». Compatible OpenPGP : le chiffrement est automatique entre comptes Proton et vers les autres services PGP. Ton courrier, c'est ta correspondance — pas une matière première publicitaire.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. C'est la meilleure porte de sortie de Google : ouvre le compte, le reste de la suite (agenda, drive, VPN) suit tout seul.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Crée un compte sur <b>proton.me</b> (l'offre gratuite suffit largement pour commencer).</li>
|
||||
<li>Rapatrie tes anciens messages et contacts avec <b>Easy Switch</b> (intégré).</li>
|
||||
<li>Mets une redirection depuis Gmail et préviens tes contacts de ta nouvelle adresse.</li>
|
||||
<li>Installe l'application mobile, active la <b>double authentification</b> — et Proton Bridge si tu tiens à Thunderbird.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Proton Pass"
|
||||
slug: "protonpass"
|
||||
category: "motsdepasse"
|
||||
difficulty: "beginner"
|
||||
url: "https://proton.me/pass"
|
||||
color: "#6d4aff"
|
||||
monogram: "Pp"
|
||||
tags: ["Avec alias e-mail","🇨🇭 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le gestionnaire de mots de passe de l'écosystème Proton, avec des alias e-mail jetables
|
||||
intégrés.
|
||||
|
||||
## why
|
||||
|
||||
Idéal si vous êtes déjà chez Proton : un seul compte pour le mail, le cloud et les mots de
|
||||
passe. Ses alias e-mail vous évitent de donner votre vraie adresse partout.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent tout regrouper chez Proton.
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Proton VPN"
|
||||
slug: "protonvpn"
|
||||
category: "vpn"
|
||||
difficulty: "beginner"
|
||||
url: "https://protonvpn.com"
|
||||
color: "#6d4aff"
|
||||
monogram: "Pv"
|
||||
iconSlug: "protonvpn"
|
||||
tags: ["Offre gratuite honnête","🇨🇭 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Le VPN de l'écosystème Proton, avec une véritable offre gratuite (rare et honnête).
|
||||
|
||||
## why
|
||||
|
||||
Basé en Suisse, audité, open-source. Son offre gratuite est sans publicité, sans revente de
|
||||
données et sans limite de volume (le nombre de pays est simplement réduit). C'est la porte
|
||||
d'entrée idéale pour un débutant, surtout si vous utilisez déjà Proton Mail.
|
||||
|
||||
## who
|
||||
|
||||
Débutants, et toute personne déjà dans l'écosystème Proton qui veut un seul compte pour tout.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Rendez-vous sur <b>protonvpn.com</b> et connectez-vous avec votre compte Proton (ou créez-en
|
||||
un).
|
||||
</li>
|
||||
<li>Installez l'application (Windows, macOS, Linux, Android, iOS).</li>
|
||||
<li>
|
||||
Utilisez « Quick Connect » ou choisissez un pays, puis activez le kill switch et NetShield
|
||||
(blocage des pubs et traqueurs).
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: "Quad9"
|
||||
slug: "quad9"
|
||||
category: "dns"
|
||||
difficulty: "beginner"
|
||||
url: "https://quad9.net"
|
||||
color: "#1a5fb4"
|
||||
monogram: "9"
|
||||
iconSlug: "quad9"
|
||||
tags: ["Anti-maliciel","🇨🇭 à but non lucratif"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un résolveur DNS chiffré, suisse et à but non lucratif, qui bloque en plus les domaines malveillants connus.
|
||||
|
||||
## why
|
||||
|
||||
Ton fournisseur d'accès voit chaque site que tu visites via le DNS. Quad9 chiffre ces requêtes, ne conserve pas ton adresse IP, et te protège de l'hameçonnage au passage. Un réglage « fais-le et oublie-le ».
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde — surtout si tu veux un gain réel sans rien installer de nouveau.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Sur Android : Réglages > Réseau > <b>DNS privé</b> > saisis <b>dns.quad9.net</b>.</li>
|
||||
<li>Sur iPhone/Mac : installe le profil DoH depuis quad9.net. Sur PC : configure le DNS-over-HTTPS dans le navigateur ou le système.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Qubes OS · postmarketOS"
|
||||
slug: "qubes"
|
||||
category: "os"
|
||||
difficulty: "advanced"
|
||||
url: "https://www.qubes-os.org"
|
||||
color: "#3874d8"
|
||||
monogram: "Qb"
|
||||
iconSlug: "qubesos"
|
||||
tags: ["Cloisonnement","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Deux systèmes de niche : Qubes cloisonne votre PC en machines virtuelles étanches ; postmarketOS
|
||||
fait tourner du Linux sur d'anciens téléphones.
|
||||
|
||||
## why
|
||||
|
||||
Qubes isole chaque activité (travail, banque, navigation risquée) dans son propre compartiment :
|
||||
une compromission n'atteint pas le reste. postmarketOS prolonge la vie de téléphones abandonnés
|
||||
par leur constructeur.
|
||||
|
||||
## who
|
||||
|
||||
Utilisateurs experts avec un modèle de menace très élevé.
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "DuckDuckGo · Brave Search · SearXNG"
|
||||
slug: "search"
|
||||
category: "navigateur"
|
||||
difficulty: "beginner"
|
||||
url: "https://duckduckgo.com"
|
||||
color: "#de5833"
|
||||
monogram: "🔎"
|
||||
iconSlug: "duckduckgo"
|
||||
tags: ["Remplace Google Search"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des moteurs de recherche qui ne profilent pas chaque requête, contrairement à Google.
|
||||
|
||||
## why
|
||||
|
||||
DuckDuckGo et Brave Search ne conservent pas d'historique lié à votre identité. Startpage vous
|
||||
sert des résultats Google sans le pistage. SearXNG va plus loin : c'est un métamoteur{' '}
|
||||
<b>auto-hébergeable</b>, la souveraineté jusque dans la barre de recherche.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. SearXNG vise ceux qui veulent héberger leur propre moteur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Dans les réglages de votre navigateur, changez le moteur par défaut pour DuckDuckGo, Brave
|
||||
Search ou Startpage.
|
||||
</li>
|
||||
<li>
|
||||
Pour SearXNG : utilisez une instance publique (annuaire sur searx.space) ou hébergez la vôtre
|
||||
(voir section 10).
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Session"
|
||||
slug: "session"
|
||||
category: "messagerie"
|
||||
difficulty: "intermediate"
|
||||
license: "GPL-3.0"
|
||||
url: "https://getsession.org"
|
||||
color: "#178a5a"
|
||||
monogram: "Se"
|
||||
iconSlug: "session"
|
||||
tags: ["Sans numéro","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un chiffrement à la Signal, mais sans numéro de téléphone et avec un routage en oignon.
|
||||
|
||||
## why
|
||||
|
||||
Votre identité est un « Session ID » aléatoire. Le trafic passe par un réseau décentralisé
|
||||
(Lokinet) qui réduit fortement les métadonnées.
|
||||
|
||||
## who
|
||||
|
||||
Quand un numéro de téléphone est un risque. Écosystème plus petit, à réserver aux échanges qui
|
||||
comptent.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez depuis <b>getsession.org</b> (toutes plateformes).
|
||||
</li>
|
||||
<li>
|
||||
« Create account » génère votre Session ID et une <b>phrase de récupération</b>.
|
||||
</li>
|
||||
<li>Notez la phrase de récupération sur papier : c'est la seule clé de votre compte.</li>
|
||||
<li>Partagez votre Session ID (ou QR) à vos contacts.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
name: "Signal"
|
||||
slug: "signal"
|
||||
category: "messagerie"
|
||||
difficulty: "beginner"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://signal.org"
|
||||
color: "#3a76f0"
|
||||
monogram: "S"
|
||||
iconSlug: "signal"
|
||||
tags: ["Remplace WhatsApp","🇺🇸 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie chiffrée pour tes textos, appels, visios et groupes. C'est le remplaçant direct de WhatsApp — aussi simple, sans Meta derrière.
|
||||
|
||||
## why
|
||||
|
||||
Chiffrement de bout en bout par défaut, à but non lucratif, protocole audité qui sert de référence mondiale. Presque aucune métadonnée gardée, et l'équipe a promis de quitter l'UE plutôt que d'installer un scan. Chaque proche que tu fais basculer, c'est une conversation de plus qui sort de la surveillance.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, dès aujourd'hui. C'est le premier geste, et le plus utile : commence par ton groupe de famille. Seule contrainte : un numéro de téléphone — que tu peux cacher derrière un nom d'utilisateur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Installe depuis <b>signal.org</b> (App Store, Google Play, ou l'APK direct sur Android).</li>
|
||||
<li>Confirme ton numéro par SMS, puis choisis un code PIN.</li>
|
||||
<li>Réglages > Confidentialité : crée un <b>nom d'utilisateur</b> pour cacher ton numéro, et active le <b>verrou d'inscription</b>.</li>
|
||||
<li>Invite tes proches — le groupe famille d'abord. Sur ordinateur, lie l'application de bureau à ton téléphone.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "SimpleX Chat"
|
||||
slug: "simplex"
|
||||
category: "messagerie"
|
||||
difficulty: "intermediate"
|
||||
url: "https://simplex.chat"
|
||||
color: "#1e63d0"
|
||||
monogram: "SX"
|
||||
iconSlug: "simplex"
|
||||
tags: ["Sans identifiant","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie chiffrée sans aucun compte, sans numéro et sans identifiant. On se connecte en
|
||||
partageant un lien ou un QR code.
|
||||
|
||||
## why
|
||||
|
||||
Par conception, même les serveurs ignorent qui parle à qui. C'est aujourd'hui l'option la plus
|
||||
protectrice contre la corrélation des métadonnées.
|
||||
|
||||
## who
|
||||
|
||||
Comptes pseudonymes, contacts sensibles, haut risque. Un peu plus jeune que Signal, avec
|
||||
quelques aspérités.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez depuis <b>simplex.chat</b> (F-Droid, Google Play, App Store ou APK).
|
||||
</li>
|
||||
<li>
|
||||
Choisissez un nom d'affichage <b>local</b> (jamais votre vrai nom : il ne quitte pas votre
|
||||
appareil).
|
||||
</li>
|
||||
<li>
|
||||
Pour ajouter un contact, partagez un <b>lien d'invitation à usage unique</b> ou un QR code.
|
||||
</li>
|
||||
<li>Activez le verrou par code, et (avancé) configurez vos propres serveurs SMP.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Matrix Synapse"
|
||||
slug: "synapse"
|
||||
category: "selfhost"
|
||||
difficulty: "advanced"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://element.io/server"
|
||||
color: "#0dbd8b"
|
||||
monogram: "Mx"
|
||||
iconSlug: "matrix"
|
||||
tags: ["Votre serveur de chat","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Votre propre serveur de messagerie Matrix, sur lequel tournent Element et les appels chiffrés.
|
||||
|
||||
## why
|
||||
|
||||
En hébergeant le serveur, vous contrôlez aussi les <b>métadonnées</b> (qui parle à qui, quand),
|
||||
le point faible de Matrix en fédération. C'est la messagerie communautaire dont vous êtes le
|
||||
seul maître.
|
||||
|
||||
## who
|
||||
|
||||
Communautés, collectifs, rédactions qui veulent leur espace fédéré indépendant.
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: "Tails"
|
||||
slug: "tails"
|
||||
category: "os"
|
||||
difficulty: "advanced"
|
||||
license: "GPL-3.0"
|
||||
url: "https://tails.net"
|
||||
color: "#56347c"
|
||||
monogram: "Ta"
|
||||
iconSlug: "tails"
|
||||
tags: ["Clé USB amnésique","🌍 · tout via Tor"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un système qui démarre depuis une clé USB, fait passer tout le trafic par Tor et ne laisse{' '}
|
||||
<b>aucune trace</b> à l'extinction.
|
||||
|
||||
## why
|
||||
|
||||
À l'arrêt, tout est oublié : c'est « amnésique ». Idéal pour un travail sensible et ponctuel sur
|
||||
un ordinateur qui n'est pas le vôtre, sans rien y laisser.
|
||||
|
||||
## who
|
||||
|
||||
Lanceurs d'alerte, journalistes, sources. L'outil de référence pour les missions à haut risque.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Téléchargez Tails sur <b>tails.net</b> et suivez l'assistant pour créer la clé USB.
|
||||
</li>
|
||||
<li>Redémarrez l'ordinateur sur cette clé. Utilisez-le, puis éteignez : tout disparaît.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "Tailscale · Headscale · WireGuard"
|
||||
slug: "tailscale"
|
||||
category: "vpn"
|
||||
difficulty: "advanced"
|
||||
license: "BSD-3-Clause"
|
||||
url: "https://tailscale.com"
|
||||
color: "#242424"
|
||||
monogram: "Ts"
|
||||
iconSlug: "tailscale"
|
||||
tags: ["Accès privé à son serveur","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un réseau privé chiffré qui relie vos appareils à votre serveur, sans jamais l'exposer sur
|
||||
l'Internet public.
|
||||
|
||||
## why
|
||||
|
||||
Au lieu d'ouvrir des ports (et de vous faire attaquer), <b>Tailscale</b> crée un tunnel
|
||||
WireGuard entre vos seuls appareils autorisés : votre serveur n'a aucune surface d'attaque
|
||||
publique. <b>Headscale</b> est la version que vous hébergez vous-même, pour ne dépendre d'aucun
|
||||
tiers. <b>WireGuard</b> seul offre le contrôle total, au prix d'une configuration manuelle.
|
||||
|
||||
## who
|
||||
|
||||
Tout auto-hébergeur. C'est la façon sûre d'atteindre son Nextcloud ou ses photos depuis
|
||||
l'extérieur.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Installez Tailscale sur votre serveur et sur votre téléphone/PC, connectez-les au même compte.
|
||||
</li>
|
||||
<li>
|
||||
Vos appareils se voient aussitôt sur un réseau privé ; accédez à votre serveur par son adresse
|
||||
Tailscale.
|
||||
</li>
|
||||
<li>Pour zéro dépendance, remplacez le serveur de coordination par Headscale, auto-hébergé.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: "Tor Browser"
|
||||
slug: "tor"
|
||||
category: "tor"
|
||||
difficulty: "advanced"
|
||||
license: "BSD-3-Clause"
|
||||
url: "https://www.torproject.org"
|
||||
color: "#7d4698"
|
||||
monogram: "Tor"
|
||||
iconSlug: "torbrowser"
|
||||
tags: ["Anonymat réseau","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un réseau qui sépare qui vous êtes de ce que vous faites en ligne, accessible via le Tor
|
||||
Browser.
|
||||
|
||||
## why
|
||||
|
||||
Votre trafic rebondit à travers plusieurs relais chiffrés dans le monde : aucun ne connaît à la
|
||||
fois votre identité et votre destination. C'est l'outil des journalistes et des sources, et le
|
||||
moyen d'atteindre un site bloqué (par exemple pour télécharger un VPN censuré).
|
||||
|
||||
## who
|
||||
|
||||
Profils à haut risque, et quiconque doit dissocier son activité de son adresse IP réelle. À
|
||||
utiliser ponctuellement, pas comme navigateur de tous les jours.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Téléchargez le Tor Browser sur <b>torproject.org</b> (Windows, macOS, Linux, Android).
|
||||
</li>
|
||||
<li>
|
||||
Lancez-le et cliquez « Se connecter ». Si Tor est bloqué dans votre pays, activez un « pont »
|
||||
(bridge).
|
||||
</li>
|
||||
<li>
|
||||
Naviguez sans maximiser la fenêtre, sans installer d'extension, et{' '}
|
||||
<b>sans vous connecter à vos vrais comptes</b>.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: "Tuta"
|
||||
slug: "tuta"
|
||||
category: "email"
|
||||
difficulty: "beginner"
|
||||
license: "GPL-3.0"
|
||||
url: "https://tuta.com"
|
||||
color: "#b01e2e"
|
||||
monogram: "Tu"
|
||||
iconSlug: "tuta"
|
||||
tags: ["Remplace Gmail","🇩🇪 · open-source · post-quantique"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une messagerie e-mail allemande, entièrement open-source et sans aucun service Google.
|
||||
|
||||
## why
|
||||
|
||||
Elle chiffre aussi <b>l'objet et une partie des métadonnées</b>, avec un chiffrement résistant à
|
||||
l'informatique quantique. Juridiction UE.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent le maximum de métadonnées chiffrées et du 100 % libre. Elle n'utilise pas PGP :
|
||||
pour écrire chiffré à l'extérieur, on protège le message par mot de passe.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Créez un compte sur <b>tuta.com</b> et installez l'application (disponible sur F-Droid).
|
||||
</li>
|
||||
<li>
|
||||
Pour écrire à un non-utilisateur de façon chiffrée : cliquez sur le cadenas et définissez un
|
||||
mot de passe, transmis à votre correspondant par un autre canal.
|
||||
</li>
|
||||
<li>Activez la double authentification.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: "uBlock Origin"
|
||||
slug: "ublock"
|
||||
category: "navigateur"
|
||||
difficulty: "beginner"
|
||||
url: "https://ublockorigin.com"
|
||||
color: "#7a0d0d"
|
||||
monogram: "uB"
|
||||
iconSlug: "ublockorigin"
|
||||
tags: ["Extension","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
L'extension qui bloque publicités, traqueurs et scripts espions. Le geste le plus rentable de tout ce guide : dix secondes, effet immédiat.
|
||||
|
||||
## why
|
||||
|
||||
La publicité en ligne est le principal vecteur de surveillance et de profilage. La bloquer, c'est couper l'essentiel du pistage — et au passage tes pages chargent plus vite et ton forfait data respire.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde, sur Firefox comme sur les navigateurs Chromium. Si tu ne fais qu'un seul geste aujourd'hui, fais celui-là.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Ouvre le magasin d'extensions de ton navigateur et cherche « uBlock Origin » (l'original, par Raymond Hill).</li>
|
||||
<li>Installe-le. Il travaille aussitôt, sans aucun réglage.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: "Jitsi Meet · Element Call"
|
||||
slug: "visio"
|
||||
category: "boiteaoutils"
|
||||
difficulty: "beginner"
|
||||
url: "https://jitsi.org"
|
||||
color: "#1d6fb8"
|
||||
monogram: "Vi"
|
||||
iconSlug: "jitsi"
|
||||
tags: ["Remplace Zoom / Meet","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des outils de visioconférence sans compte ni installation lourde, pour remplacer Zoom, Google
|
||||
Meet ou Teams.
|
||||
|
||||
## why
|
||||
|
||||
<b>Jitsi Meet</b> se lance dans le navigateur, sans compte, et peut être auto-hébergé (c'est
|
||||
aussi la base de Brave Talk). <b>Element Call</b> est chiffré de bout en bout et fédéré via
|
||||
Matrix. Pour un appel simple et totalement chiffré, les appels <b>Signal</b> font aussi le
|
||||
travail.
|
||||
|
||||
## who
|
||||
|
||||
Tout le monde. Sur un serveur Jitsi public, les appels à plusieurs sont chiffrés en transport ;
|
||||
le chiffrement de bout en bout est en option.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Jitsi : allez sur meet.jit.si, créez un nom de salon, partagez le lien. Rien à installer.
|
||||
</li>
|
||||
<li>Element Call : depuis l'application Element (Matrix), lancez un appel dans un salon.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: "Votre propre site web"
|
||||
slug: "website"
|
||||
category: "selfhost"
|
||||
difficulty: "intermediate"
|
||||
url: "https://astro.build"
|
||||
color: "#7a4ddb"
|
||||
monogram: "www"
|
||||
tags: ["Résister au bannissement"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Un site à vous, sur votre nom de domaine, où votre contenu ne dépend d'aucune plateforme.
|
||||
|
||||
## why
|
||||
|
||||
Le jour où un réseau supprime ou suspend votre compte, tout votre travail disparaît. Un site
|
||||
personnel est votre point d'ancrage : vos articles y restent, archivés et à jour, quoi qu'il
|
||||
arrive. C'est exactement ce que font les comptes d'information prudents. Un site statique
|
||||
(généré par Hugo, Astro ou Jekyll) suffit et se publie presque gratuitement ; pour un blog
|
||||
dynamique, un WordPress ou un Ghost auto-hébergé fait l'affaire.
|
||||
|
||||
## who
|
||||
|
||||
Comptes d'information, créateurs, militants, quiconque publie et craint la censure de
|
||||
plateforme.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Achetez un nom de domaine (idéalement chez un registraire respectueux de la vie privée).
|
||||
</li>
|
||||
<li>
|
||||
Choisissez un générateur de site statique (Astro, Hugo) et hébergez le résultat, ou installez
|
||||
WordPress/Ghost sur votre serveur.
|
||||
</li>
|
||||
<li>
|
||||
Renvoyez-y systématiquement depuis vos réseaux : votre audience vous retrouve même si un
|
||||
compte tombe.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: "YubiKey · Nitrokey"
|
||||
slug: "yubikey"
|
||||
category: "deuxfa"
|
||||
difficulty: "intermediate"
|
||||
url: "https://www.yubico.com"
|
||||
color: "#84bd00"
|
||||
monogram: "Yk"
|
||||
iconSlug: "yubico"
|
||||
tags: ["Clé matérielle FIDO2"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Une petite clé physique (format USB, souvent avec NFC) qui prouve votre identité d'un simple
|
||||
contact du doigt.
|
||||
|
||||
## why
|
||||
|
||||
C'est la 2FA la plus solide : elle résiste à l'hameçonnage, car la clé vérifie l'adresse réelle
|
||||
du site avant de répondre. <b>Nitrokey</b> est l'alternative à matériel et micrologiciel
|
||||
entièrement ouverts. Un faux site ne peut pas vous piéger.
|
||||
|
||||
## who
|
||||
|
||||
Ceux qui veulent le maximum de sécurité sur leurs comptes clés. Achetez-en <b>deux</b> (une
|
||||
principale, une de secours).
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>Procurez-vous deux clés sur yubico.com ou nitrokey.com.</li>
|
||||
<li>
|
||||
Dans les réglages de sécurité de chaque compte (e-mail, gestionnaire de mots de passe…),
|
||||
ajoutez une « clé de sécurité » ou « passkey ». Enregistrez vos deux clés.
|
||||
</li>
|
||||
<li>Gardez la clé de secours dans un lieu sûr, séparé.</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: "YunoHost · Umbrel · Start9"
|
||||
slug: "yunohost"
|
||||
category: "selfhost"
|
||||
difficulty: "advanced"
|
||||
license: "AGPL-3.0"
|
||||
url: "https://yunohost.org"
|
||||
color: "#e29100"
|
||||
monogram: "Yh"
|
||||
tags: ["Serveur clé en main","🌍 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
|
||||
Des systèmes qui transforment un vieux PC ou un Raspberry Pi en serveur personnel, avec un
|
||||
catalogue d'applications à installer en quelques clics.
|
||||
|
||||
## why
|
||||
|
||||
L'auto-hébergement fait peur, mais ces outils font le gros du travail. <b>YunoHost</b> gère même
|
||||
votre propre e-mail et une authentification unique. <b>Umbrel</b> offre un magasin de plus de
|
||||
300 applications, très prisé des amateurs de vie privée et de Bitcoin. <b>Start9</b> chiffre les
|
||||
sauvegardes et donne une adresse Tor à chaque service par défaut.
|
||||
|
||||
## who
|
||||
|
||||
Les profils avancés prêts à consacrer un après-midi à monter leur propre cloud. C'est la parade
|
||||
structurelle la plus radicale à Chat Control.
|
||||
|
||||
## install
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
Récupérez un vieil ordinateur, un Raspberry Pi, ou louez un petit serveur (VPS) dans un pays
|
||||
protecteur.
|
||||
</li>
|
||||
<li>Installez YunoHost, Umbrel ou Start9 en suivant leur guide officiel.</li>
|
||||
<li>Depuis le catalogue, installez Nextcloud, un serveur Matrix, une galerie photo, etc.</li>
|
||||
<li>
|
||||
Accédez-y en privé avec Tailscale (section VPN) plutôt que d'exposer le serveur au public.
|
||||
</li>
|
||||
</ol>
|
||||
@@ -0,0 +1,90 @@
|
||||
# Guide de rédaction — fiches d'outils FR/créole
|
||||
|
||||
## Format d'une fiche (`content/outils/<slug>.md`)
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: "Signal"
|
||||
slug: "signal"
|
||||
category: "messagerie" # slug d'une catégorie de content/categories.json
|
||||
difficulty: "beginner" # beginner | intermediate | advanced
|
||||
license: "AGPL-3.0" # identifiant SPDX exact (spdx.org/licenses)
|
||||
url: "https://signal.org"
|
||||
color: "#3a76f0" # couleur du monogramme (décorative)
|
||||
monogram: "S"
|
||||
tags: ["Remplace WhatsApp","🇺🇸 · open-source"]
|
||||
lang: "fr"
|
||||
---
|
||||
|
||||
## what
|
||||
À quoi sert l'outil, en deux phrases maximum.
|
||||
|
||||
## why
|
||||
Pourquoi il compte pour la souveraineté numérique.
|
||||
|
||||
## who
|
||||
Pour qui, et à quel moment du parcours.
|
||||
|
||||
## install
|
||||
<ol><li>Étapes concrètes, numérotées.</li></ol>
|
||||
```
|
||||
|
||||
Les valeurs du frontmatter sont encodées en JSON (guillemets doubles).
|
||||
Le corps accepte Markdown et HTML simple ; il est rendu au build, rien n'est
|
||||
interprété côté client. **Le build échoue si une fiche est invalide** — c'est
|
||||
le contrat de validation (ADR-001).
|
||||
|
||||
## Ton éditorial
|
||||
|
||||
- **Militant mais chaleureux, ancré Caraïbe.** Pas de langage corporate, pas
|
||||
de peur gratuite : on explique le geste, on donne le pouvoir d'agir.
|
||||
- Tutoiement. Phrases courtes. Le premier mot de chaque section porte le sens
|
||||
(motif en F, doctrine §1.1).
|
||||
- `what` dit le service rendu, `why` dit l'enjeu politique/technique, `who`
|
||||
donne la porte d'entrée (« commence ici si… »), `install` est actionnable
|
||||
sans autre lecture.
|
||||
- Exemples et localisation ancrés territoire — jamais de Lorem ipsum
|
||||
hexagonal (doctrine §4.2).
|
||||
|
||||
### Fiches de référence pour le ton
|
||||
|
||||
Six fiches sont réécrites au registre OKI et servent de modèle : `signal`,
|
||||
`protonmail`, `brave`, `bitwarden`, `ublock`, `quad9`. Avant d'écrire ou de
|
||||
réviser une fiche, relis-en deux. Ce qui change par rapport au ton d'origine :
|
||||
tutoiement systématique, le geste avant la technique (« dix secondes, effet
|
||||
immédiat »), l'enjeu politique dit sans détour (« ta correspondance — pas une
|
||||
matière première publicitaire »), et un ancrage collectif (« commence par ton
|
||||
groupe de famille »). Le script de migration ne régénère jamais une fiche
|
||||
existante (`--force` pour tout écraser) : les réécritures sont protégées.
|
||||
|
||||
## Créole guadeloupéen (gcf)
|
||||
|
||||
- Les **micro-textes de l'interface** (boutons, filtres, états vides, marqueur
|
||||
de fin) vivent dans `frontend/src/lib/i18n/gcf.json`. C'est la couche à
|
||||
plus fort impact affectif pour le plus faible coût (doctrine §4.2) — elle
|
||||
est traduite en premier.
|
||||
- ⚠️ **Relecture par un locuteur natif requise** : les chaînes gcf actuelles
|
||||
sont un premier jet à valider avant mise en production. La graphie de
|
||||
référence à trancher (GEREC-F ou autre) doit être notée ici une fois choisie.
|
||||
- Chaque fiche peut avoir sa **jumelle créole** : `content/outils/gcf/<slug>.md`
|
||||
(frontmatter minimal : `name`, `slug`, `lang: "gcf"` ; mêmes sections
|
||||
`## what/why/who/install`). Elle s'affiche automatiquement quand la personne
|
||||
choisit Kréyòl, avec un bandeau « tradiksyon an chantyé » ; sans jumelle,
|
||||
la version française s'affiche avec un bandeau « póko tradui ». Six jumelles
|
||||
pilotes existent (mêmes slugs que les fiches de référence ci-dessus) —
|
||||
**premier jet non relu**, à valider avant d'en faire des modèles.
|
||||
- Termes fixés : « zouti » (outil), « Ou pa manké ayen » (fin de liste),
|
||||
« Chwazi zouti ou » (héros), niveaux « Débitan / Entèmédia / Avansé ».
|
||||
|
||||
## Ajouter une catégorie
|
||||
|
||||
Ajouter une entrée à `content/categories.json` (`slug`, `title`, `order`,
|
||||
`part`). La navigation n'affiche que 4 catégories avant la divulgation
|
||||
progressive (Hick-Hyman, doctrine §1.3) : l'ordre `order` décide lesquelles.
|
||||
|
||||
## Vérifications avant merge
|
||||
|
||||
- [ ] `license` est un identifiant SPDX valide
|
||||
- [ ] `difficulty` reflète l'expérience réelle d'installation (pas le prestige)
|
||||
- [ ] Chaque section respecte sa question (what/why/who/install)
|
||||
- [ ] `pnpm build` passe (validation du frontmatter incluse)
|
||||
@@ -0,0 +1,140 @@
|
||||
# Déployer lage-chat-control sur un VPS
|
||||
|
||||
Le site est **100 % statique** (ADR-001) : le déploiement consiste à poser le
|
||||
contenu du dossier `frontend/build/` derrière un serveur web. Pas de base de
|
||||
données, pas de service Node à maintenir, pas de tâche cron. Un miroir du site
|
||||
= une copie du dossier.
|
||||
|
||||
## 0. Prérequis
|
||||
|
||||
- Un VPS (1 vCPU / 512 Mo suffisent très largement) sous Debian/Ubuntu
|
||||
- Un nom de domaine pointant sur le VPS (enregistrement A/AAAA)
|
||||
- En local : Node ≥ 22.12 et pnpm (le build se fait **sur ta machine**,
|
||||
le VPS n'a besoin ni de Node ni de pnpm)
|
||||
|
||||
## 1. Construire le site
|
||||
|
||||
```sh
|
||||
git clone https://labola.o-k-i.net/cyber-mawonaj/lage-chat-control.git
|
||||
cd lage-chat-control/frontend
|
||||
pnpm install
|
||||
pnpm build # produit frontend/build/ (~60 pages statiques)
|
||||
```
|
||||
|
||||
## 2. Option A — Caddy (recommandé : TLS automatique)
|
||||
|
||||
```sh
|
||||
# sur le VPS
|
||||
sudo apt install caddy
|
||||
sudo mkdir -p /srv/lage-chat-control
|
||||
```
|
||||
|
||||
`/etc/caddy/Caddyfile` :
|
||||
|
||||
```caddy
|
||||
exemple.o-k-i.net {
|
||||
root * /srv/lage-chat-control
|
||||
file_server
|
||||
encode zstd gzip
|
||||
|
||||
# SvelteKit adapter-static : pages pré-générées + fallback
|
||||
try_files {path} {path}.html {path}/index.html /404.html
|
||||
|
||||
header {
|
||||
# le service worker doit pouvoir se mettre à jour
|
||||
Cache-Control "no-cache" /service-worker.js
|
||||
# assets immuables (noms hachés)
|
||||
Cache-Control "public, max-age=31536000, immutable" /_app/immutable/*
|
||||
# en-têtes de sécurité
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
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'"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Le TLS (Let's Encrypt) est automatique, rien d'autre à faire.
|
||||
|
||||
## 3. Option B — nginx
|
||||
|
||||
```sh
|
||||
sudo apt install nginx certbot python3-certbot-nginx
|
||||
```
|
||||
|
||||
`/etc/nginx/sites-available/lage-chat-control` :
|
||||
|
||||
```nginx
|
||||
server {
|
||||
server_name exemple.o-k-i.net;
|
||||
root /srv/lage-chat-control;
|
||||
index index.html;
|
||||
|
||||
# pages pré-générées : /outils → outils.html, fallback 404
|
||||
location / {
|
||||
try_files $uri $uri.html $uri/index.html /404.html;
|
||||
}
|
||||
|
||||
location /_app/immutable/ {
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
}
|
||||
|
||||
location = /service-worker.js {
|
||||
add_header Cache-Control "no-cache";
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/html text/css application/javascript application/json image/svg+xml;
|
||||
}
|
||||
```
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## 4. Publier (et republier)
|
||||
|
||||
```sh
|
||||
# depuis ta machine, après chaque `pnpm build`
|
||||
rsync -avz --delete frontend/build/ vps:/srv/lage-chat-control/
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Le site s'installe avec 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.
|
||||
|
||||
YunoHost gère TLS, HSTS et le renouvellement des certificats.
|
||||
|
||||
## 6. Option D — Docker
|
||||
|
||||
Le `Dockerfile` et `docker/nginx.conf` du dépôt (hérités du site Astro)
|
||||
servent déjà un site statique durci ; adapter le chemin de copie vers
|
||||
`frontend/build/` si tu veux cette voie. Utile pour l'infra OKI si tout
|
||||
le reste tourne déjà en conteneurs — sinon les options A/B sont plus simples.
|
||||
|
||||
## 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)
|
||||
- [ ] 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
|
||||
- [ ] Aucune requête sortante vers un domaine tiers (onglet Réseau)
|
||||
@@ -0,0 +1,72 @@
|
||||
# Design system OKI — exitchatcontrol.org
|
||||
|
||||
Toutes les valeurs visuelles vivent dans
|
||||
[`frontend/src/styles/tokens.css`](../frontend/src/styles/tokens.css).
|
||||
Les composants Svelte consomment les tokens ; **jamais de valeur brute**
|
||||
(couleur, taille) dans un composant (doctrine §5.2).
|
||||
|
||||
## Architecture CSS
|
||||
|
||||
Cascade explicite par `@layer` ([app.css](../frontend/src/app.css)) :
|
||||
|
||||
```
|
||||
reset → tokens → base → layouts → components → utilities
|
||||
```
|
||||
|
||||
- `reset.css` — reset moderne minimal (~40 lignes), inclut `prefers-reduced-motion`.
|
||||
- `tokens.css` — custom properties : couleurs `oklch()`, typo fluide `clamp()`, espacements, interaction.
|
||||
- `base.css` — HTML sémantique nu : typo, liens, focus visible, boutons.
|
||||
- `layouts.css` — primitives Every Layout : `.stack`, `.cluster`, `.center`, `.card-grid` (container queries, pas de media queries de largeur d'écran).
|
||||
- Le style propre à chaque composant vit dans son `<style>` Svelte (scopé à la compilation).
|
||||
|
||||
## Couleurs
|
||||
|
||||
En `oklch()` : la luminance perceptuelle rend les contrastes WCAG calculables
|
||||
(méthode modus-vivendi, doctrine §4.3). `light-dark()` + `color-scheme` gèrent
|
||||
le mode sombre sans duplication.
|
||||
|
||||
| Token | Rôle | Note contraste |
|
||||
|---|---|---|
|
||||
| `--oki-ink` / `--oki-surface` | texte / fond | ≥ 7:1 (AAA) dans les deux modes |
|
||||
| `--oki-ink-2` | texte secondaire | ≥ 4.5:1 (AA) |
|
||||
| `--oki-jaune` `--oki-vert` `--oki-rouge` `--oki-noir` | identité panafricaine | décoratif — jamais porteur d'information seul |
|
||||
| `--accent` / `--accent-ink` | accent actif / accent texte | `--accent-ink` ≥ 4.5:1 sur `--oki-surface` |
|
||||
| `--diff-*` / `--diff-*-ink` | difficulté (fond / texte) | les variantes `-ink` sont AA sur `--oki-surface` |
|
||||
|
||||
**Règle absolue** : la difficulté n'est jamais codée par la couleur seule —
|
||||
`DifficultyBadge` = forme (●/◆/▲) + texte + couleur (doctrine §1.1, ~8 % de
|
||||
déficience de vision des couleurs).
|
||||
|
||||
## Typographie
|
||||
|
||||
- Corps : **Atkinson Hyperlegible** 400 (SIL OFL, conçue pour la lisibilité).
|
||||
- Display (titres, marque) : **Fraunces** 700 (SIL OFL).
|
||||
- 2 fichiers WOFF2 auto-hébergés (`static/fonts/`), ~35 Ko au total,
|
||||
`font-display: swap` — jamais de Google Fonts (doctrine §5.3).
|
||||
- Échelle modulaire ratio 1.25, fluide par `clamp()` : `--text-0` … `--text-5`.
|
||||
- `--measure: 65ch` sur `p, li` et `.prose` (doctrine §2.2 : 45–75 caractères).
|
||||
- Tout en `rem` : la taille utilisateur est respectée.
|
||||
|
||||
## Interaction
|
||||
|
||||
- `--tap-target: 48px` : tout élément interactif (boutons, liens de nav,
|
||||
filtres, switcher) a une hauteur minimale de 48 px (Fitts, doctrine §1.3).
|
||||
- `:focus-visible` global : anneau 3 px `--accent`, offset 2 px.
|
||||
- Durées `--duration-1/2` + `--ease-out` ; annulées par `prefers-reduced-motion`.
|
||||
|
||||
## Composants
|
||||
|
||||
| Composant | Rôle | Principe doctrinal |
|
||||
|---|---|---|
|
||||
| `DifficultyBadge` | niveau (forme + texte + couleur) | §1.1 — jamais couleur seule |
|
||||
| `ToolCard` | fiche condensée, vrai `<a>` couvrant | §4.3 — pas de div cliquable |
|
||||
| `CategoryNav` | 4 catégories visibles + `<details>` | §1.3 Hick-Hyman, divulgation progressive, sans JS |
|
||||
| `SearchBar` | recherche instantanée, raccourci `/` | §2.2 — le clavier est roi |
|
||||
| `LanguageSwitcher` | FR / Kréyòl, persisté localStorage | §4.2 — bilinguisme |
|
||||
| `EndMarker` | « Ou pa manké ayen » en fin de liste | §1.5, §3 — point d'arrêt explicite |
|
||||
|
||||
## Test des 50 ms (doctrine §1.1)
|
||||
|
||||
L'identité OKI est reconnaissable avant toute lecture : bandes panafricaines
|
||||
vert/jaune/rouge dans la marque d'en-tête, soulignés et bordures jaune OKI,
|
||||
titres Fraunces, créole visible dès le héros (« Chwazi zouti ou »).
|
||||
@@ -0,0 +1,40 @@
|
||||
# exitchatcontrol × OKI — frontend
|
||||
|
||||
Annuaire souverain d'outils libres. SvelteKit 2 + Svelte 5, site 100 %
|
||||
prérendu, zéro CDN tiers, zéro tracker. Voir [`ADR.md`](../ADR.md) pour la
|
||||
décision d'architecture et `../docs/` pour le design system et le guide
|
||||
de contenu.
|
||||
|
||||
## Prérequis
|
||||
|
||||
Node ≥ 22.12, pnpm.
|
||||
|
||||
## Commandes
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
pnpm migrate # (re)génère content/ depuis l'ancien site Astro (src/)
|
||||
pnpm dev # serveur de développement
|
||||
pnpm build # build statique dans build/ (prérend les 58 pages)
|
||||
pnpm preview # sert le build localement
|
||||
pnpm check # svelte-check (types + a11y)
|
||||
```
|
||||
|
||||
## Déploiement
|
||||
|
||||
`build/` est un site statique : n'importe quel serveur de fichiers convient
|
||||
(nginx du `docker/` existant, YunoHost `my_webapp`, etc.). Aucune dépendance
|
||||
cloud, aucun service à maintenir — un miroir = une copie du dossier.
|
||||
|
||||
## Arborescence
|
||||
|
||||
```
|
||||
src/styles/ tokens, reset, base, layouts (cascade @layer)
|
||||
src/lib/i18n/ fr.json + gcf.json typés, store de langue (runes)
|
||||
src/lib/components/ ToolCard, DifficultyBadge, CategoryNav, SearchBar,
|
||||
LanguageSwitcher, EndMarker
|
||||
src/lib/content/ chargeur build-time des fichiers ../content/
|
||||
src/routes/ / (landing), /outils (annuaire filtrable), /outils/[slug]
|
||||
static/fonts/ 2 WOFF2 auto-hébergés (Atkinson Hyperlegible, Fraunces)
|
||||
scripts/ migrate-tools.mjs
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "exitchatcontrol-oki",
|
||||
"private": true,
|
||||
"version": "4.0.0-alpha.1",
|
||||
"type": "module",
|
||||
"description": "Exit Chat Control × OKI — annuaire souverain d'outils libres. SvelteKit statique, zéro tracker, zéro CDN.",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"migrate": "node scripts/migrate-tools.mjs",
|
||||
"audit:a11y": "node scripts/audit-a11y.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "^4.12.1",
|
||||
"@fontsource/atkinson-hyperlegible": "^5.2.8",
|
||||
"@fontsource/fraunces": "^5.2.9",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@sveltejs/adapter-static": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
"@types/node": "^26.1.1",
|
||||
"marked": "^16.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"typescript": "^5.6.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
Generated
+1133
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
// Audit axe-core des pages clés servies sur localhost:4173
|
||||
import { chromium } from '@playwright/test'
|
||||
import { AxeBuilder } from '@axe-core/playwright'
|
||||
|
||||
const PAGES = ['/', '/outils', '/outils/signal', '/outils/tor', '/contribuer']
|
||||
const browser = await chromium.launch()
|
||||
const context = await browser.newContext()
|
||||
const page = await context.newPage()
|
||||
let total = 0
|
||||
|
||||
for (const path of PAGES) {
|
||||
await page.goto(`http://localhost:4173${path}`, { waitUntil: 'networkidle' })
|
||||
const results = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag22aa']).analyze()
|
||||
total += results.violations.length
|
||||
console.log(`\n═══ ${path} — ${results.violations.length} violation(s)`)
|
||||
for (const v of results.violations) {
|
||||
console.log(` [${v.impact}] ${v.id}: ${v.help}`)
|
||||
for (const n of v.nodes.slice(0, 3)) console.log(` → ${n.target.join(' ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close()
|
||||
console.log(`\nTOTAL: ${total} violation(s)`)
|
||||
process.exit(total > 0 ? 1 : 0)
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Migration des données du site Astro vers les fichiers plats OKI (ADR-001,
|
||||
* mission §7). Extrait les 55 ToolCard des MDX français + métadonnées de
|
||||
* src/data/{tools,directory}.json et produit :
|
||||
* content/outils/<slug>.md (frontmatter JSON-par-ligne + sections HTML)
|
||||
* content/categories.json (catégories dérivées des sections sources)
|
||||
*
|
||||
* Idempotent : relançable, écrase la sortie. Usage : pnpm migrate
|
||||
*/
|
||||
import { readFile, readdir, writeFile, mkdir } from 'node:fs/promises'
|
||||
import { existsSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||
const SRC = path.join(ROOT, 'src')
|
||||
const OUT = path.join(ROOT, 'content')
|
||||
|
||||
const DIFFICULTY = { 1: 'beginner', 2: 'intermediate', 3: 'advanced' }
|
||||
|
||||
const tools = JSON.parse(await readFile(path.join(SRC, 'data', 'tools.json'), 'utf8'))
|
||||
const directory = JSON.parse(await readFile(path.join(SRC, 'data', 'directory.json'), 'utf8'))
|
||||
const toolById = new Map(tools.map((t) => [t.id, t]))
|
||||
const dirById = new Map(directory.map((d) => [d.id, d]))
|
||||
|
||||
function parseFrontmatter(raw) {
|
||||
const m = raw.match(/^---\n([\s\S]*?)\n---\n?/)
|
||||
if (!m) return [{}, raw]
|
||||
const meta = {}
|
||||
for (const line of m[1].split('\n')) {
|
||||
const i = line.indexOf(':')
|
||||
if (i === -1) continue
|
||||
const key = line.slice(0, i).trim()
|
||||
let value = line.slice(i + 1).trim()
|
||||
if (/^'.*'$/.test(value) || /^".*"$/.test(value)) value = value.slice(1, -1)
|
||||
meta[key] = /^\d+$/.test(value) ? Number(value) : value
|
||||
}
|
||||
return [meta, raw.slice(m[0].length)]
|
||||
}
|
||||
|
||||
/** Extrait le contenu d'un slot (Fragment/ol/ul) dans un bloc ToolCard. */
|
||||
function slot(block, name) {
|
||||
const re = new RegExp(
|
||||
`<(Fragment|ol|ul)([^>]*slot="${name}"[^>]*)>([\\s\\S]*?)</\\1>`
|
||||
)
|
||||
const m = block.match(re)
|
||||
if (!m) return null
|
||||
// Les listes gardent leur balise (le HTML passe tel quel dans le markdown) ;
|
||||
// les Fragments sont déballés.
|
||||
return m[1] === 'Fragment' ? m[3].trim() : `<${m[1]}>${m[3]}</${m[1]}>`.trim()
|
||||
}
|
||||
|
||||
/** Tags = textes des <span class="tool-tag"> hors difficulté (déjà structurée). */
|
||||
function extractTags(block) {
|
||||
const html = slot(block, 'tags') ?? ''
|
||||
const tags = [...html.matchAll(/<span class="tool-tag">([\s\S]*?)<\/span>/g)]
|
||||
.map((m) => m[1].replace(/\s+/g, ' ').trim())
|
||||
.filter((t) => !/^[🟢🟡🔴]/u.test(t))
|
||||
return tags
|
||||
}
|
||||
|
||||
/** Difficulté : `level` de tools.json, sinon l'emoji du tag MDX (🟢/🟡/🔴). */
|
||||
function difficulty(tool, block) {
|
||||
if (DIFFICULTY[tool.level]) return DIFFICULTY[tool.level]
|
||||
const html = slot(block, 'tags') ?? ''
|
||||
if (html.includes('🟢')) return 'beginner'
|
||||
if (html.includes('🟡')) return 'intermediate'
|
||||
if (html.includes('🔴')) return 'advanced'
|
||||
console.warn(`⚠ ${tool.id} : difficulté introuvable — « intermediate » par défaut`)
|
||||
return 'intermediate'
|
||||
}
|
||||
|
||||
const fm = (key, value) => (value == null ? null : `${key}: ${JSON.stringify(value)}`)
|
||||
|
||||
const sectionsDir = path.join(SRC, 'content', 'sections', 'fr')
|
||||
const files = (await readdir(sectionsDir)).filter((f) => f.endsWith('.mdx')).sort()
|
||||
|
||||
await mkdir(path.join(OUT, 'outils'), { recursive: true })
|
||||
|
||||
const categories = []
|
||||
const seen = new Set()
|
||||
let written = 0
|
||||
|
||||
for (const file of files) {
|
||||
const raw = await readFile(path.join(sectionsDir, file), 'utf8')
|
||||
const [meta, body] = parseFrontmatter(raw)
|
||||
const blocks = [...body.matchAll(/<ToolCard tool="([^"]+)"[^>]*>([\s\S]*?)<\/ToolCard>/g)]
|
||||
if (blocks.length === 0) continue
|
||||
|
||||
categories.push({
|
||||
slug: meta.id,
|
||||
title: meta.title,
|
||||
order: meta.order,
|
||||
part: meta.part
|
||||
})
|
||||
|
||||
for (const [, toolId, block] of blocks) {
|
||||
const tool = toolById.get(toolId)
|
||||
if (!tool) {
|
||||
console.warn(`⚠ ${file} : ToolCard "${toolId}" absent de tools.json — ignoré`)
|
||||
continue
|
||||
}
|
||||
const slug = toolId.replace(/^t-/, '')
|
||||
if (seen.has(slug)) {
|
||||
console.warn(`⚠ ${slug} : doublon (déjà émis) — ignoré dans ${file}`)
|
||||
continue
|
||||
}
|
||||
seen.add(slug)
|
||||
// ne jamais écraser une fiche retravaillée à la main (--force pour tout régénérer)
|
||||
const outPath = path.join(OUT, 'outils', `${slug}.md`)
|
||||
if (existsSync(outPath) && !process.argv.includes('--force')) {
|
||||
written++
|
||||
continue
|
||||
}
|
||||
const dir = dirById.get(slug)
|
||||
const nameOverride = slot(block, 'name')
|
||||
|
||||
const front = [
|
||||
fm('name', nameOverride ?? tool.name),
|
||||
fm('slug', slug),
|
||||
fm('category', meta.id),
|
||||
fm('difficulty', difficulty(tool, block)),
|
||||
fm('license', dir?.license),
|
||||
fm('url', tool.url),
|
||||
fm('color', tool.color),
|
||||
fm('monogram', tool.monogram),
|
||||
fm('iconSlug', tool.iconSlug),
|
||||
fm('tags', extractTags(block)),
|
||||
fm('lang', 'fr')
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
|
||||
const sections = [
|
||||
['what', slot(block, 'what')],
|
||||
['why', slot(block, 'why')],
|
||||
['who', slot(block, 'who')],
|
||||
['install', slot(block, 'install')],
|
||||
['extra', slot(block, 'extra')]
|
||||
]
|
||||
.filter(([, html]) => html)
|
||||
.map(([key, html]) => `## ${key}\n\n${html}`)
|
||||
.join('\n\n')
|
||||
|
||||
await writeFile(outPath, `---\n${front}\n---\n\n${sections}\n`)
|
||||
written++
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile(path.join(OUT, 'categories.json'), JSON.stringify(categories, null, 2) + '\n')
|
||||
console.log(`✔ ${written} fiches écrites dans content/outils/, ${categories.length} catégories`)
|
||||
|
||||
const missing = tools.filter((t) => !seen.has(t.id.replace(/^t-/, '')))
|
||||
if (missing.length) {
|
||||
console.warn(`⚠ outils de tools.json sans ToolCard fr : ${missing.map((t) => t.id).join(', ')}`)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* app.css — ordre de cascade explicite et auditable (doctrine §5.2).
|
||||
* Chaque couche est déclarée AVANT tout import : la position dans la
|
||||
* cascade est ici, pas dans l'ordre des fichiers.
|
||||
*/
|
||||
@layer reset, tokens, base, layouts, components, utilities;
|
||||
|
||||
/* @font-face n'est pas concerné par les couches */
|
||||
@import './styles/fonts.css';
|
||||
|
||||
@import './styles/reset.css' layer(reset);
|
||||
@import './styles/tokens.css' layer(tokens);
|
||||
@import './styles/base.css' layer(base);
|
||||
@import './styles/layouts.css' layer(layouts);
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#17181f" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/site.webmanifest" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { browser } from '$app/environment'
|
||||
|
||||
/**
|
||||
* « Jès fèt » — les outils que la personne a adoptés.
|
||||
* Effet Zeigarnik utilisé positivement (doctrine §1.2) : une progression
|
||||
* choisie, pas un streak. Stocké uniquement sur l'appareil, jamais envoyé
|
||||
* nulle part (pas de compte, pas de télémétrie — doctrine §3.3 : aucune
|
||||
* métrique anxiogène ; le compteur n'apparaît qu'après le premier geste).
|
||||
*/
|
||||
const STORAGE_KEY = 'oki-jes'
|
||||
|
||||
function initial(): string[] {
|
||||
if (!browser) return []
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
return raw ? (JSON.parse(raw) as string[]) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const state = $state({ slugs: initial() })
|
||||
|
||||
function persist() {
|
||||
if (browser) localStorage.setItem(STORAGE_KEY, JSON.stringify(state.slugs))
|
||||
}
|
||||
|
||||
export function isAdopted(slug: string): boolean {
|
||||
return state.slugs.includes(slug)
|
||||
}
|
||||
|
||||
export function adoptedCount(): number {
|
||||
return state.slugs.length
|
||||
}
|
||||
|
||||
export function toggleAdopted(slug: string) {
|
||||
state.slugs = isAdopted(slug) ? state.slugs.filter((s) => s !== slug) : [...state.slugs, slug]
|
||||
persist()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { isAdopted, toggleAdopted } from '$lib/adopted.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { slug }: { slug: string } = $props()
|
||||
let adopted = $derived(isAdopted(slug))
|
||||
</script>
|
||||
|
||||
<!-- Amélioration progressive : sans JS le bouton n'apparaît pas du tout
|
||||
(rendu identique, aucune fonction cassée). -->
|
||||
<button type="button" class:adopted aria-pressed={adopted} onclick={() => toggleAdopted(slug)}>
|
||||
{adopted ? t('adopted.marked') : t('adopted.mark')}
|
||||
<span class="visually-hidden">{t('adopted.srHint')}</span>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
border: 2px solid var(--oki-vert);
|
||||
background: transparent;
|
||||
color: var(--oki-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button.adopted {
|
||||
background: var(--oki-vert);
|
||||
color: oklch(98% 0 0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { page } from '$app/state'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
interface NavCategory {
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
let { categories }: { categories: NavCategory[] } = $props()
|
||||
|
||||
// Hick-Hyman (doctrine §1.3) : ≤ 5 entrées visibles, le reste en
|
||||
// divulgation progressive (<details>, fonctionne sans JS).
|
||||
const VISIBLE = 4
|
||||
let visible = $derived(categories.slice(0, VISIBLE))
|
||||
let rest = $derived(categories.slice(VISIBLE))
|
||||
// searchParams non lisibles au prerender : garde `browser`
|
||||
let current = $derived(browser ? page.url.searchParams.get('cat') : null)
|
||||
|
||||
function href(slug: string | null): string {
|
||||
return slug ? `/outils?cat=${slug}` : '/outils'
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav aria-label={t('directory.filterCategory')} class="catnav">
|
||||
<ul class="cluster">
|
||||
<li><a href={href(null)} aria-current={current === null ? 'page' : undefined}>{t('directory.allCategories')}</a></li>
|
||||
{#each visible as cat (cat.slug)}
|
||||
<li>
|
||||
<a href={href(cat.slug)} aria-current={current === cat.slug ? 'page' : undefined}>{cat.title}</a>
|
||||
</li>
|
||||
{/each}
|
||||
{#if rest.length > 0}
|
||||
<li>
|
||||
<details>
|
||||
<summary>+ {rest.length}</summary>
|
||||
<ul class="stack">
|
||||
{#each rest as cat (cat.slug)}
|
||||
<li>
|
||||
<a href={href(cat.slug)} aria-current={current === cat.slug ? 'page' : undefined}>{cat.title}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
a[aria-current='page'] {
|
||||
background: var(--oki-jaune);
|
||||
border-color: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
details {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px dashed var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details ul {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset-inline-start: 0;
|
||||
margin-block-start: var(--space-1);
|
||||
padding: var(--space-2);
|
||||
background: var(--oki-surface);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
min-inline-size: 16rem;
|
||||
box-shadow: 0 4px 16px oklch(0% 0 0 / 0.15);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
// Jamais la couleur seule : forme + texte + couleur (doctrine §1.1 —
|
||||
// ~8 % de déficience de vision des couleurs).
|
||||
const SHAPES = { beginner: '●', intermediate: '◆', advanced: '▲' } as const
|
||||
|
||||
let { difficulty }: { difficulty: keyof typeof SHAPES } = $props()
|
||||
</script>
|
||||
|
||||
<span class="badge badge-{difficulty}">
|
||||
<span aria-hidden="true">{SHAPES[difficulty]}</span>
|
||||
{t(`difficulty.${difficulty}`)}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-size: var(--text-0);
|
||||
font-weight: 600;
|
||||
padding: 0.15em 0.6em;
|
||||
border-radius: var(--radius-2);
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
.badge-beginner {
|
||||
color: var(--diff-beginner-ink);
|
||||
}
|
||||
.badge-intermediate {
|
||||
color: var(--diff-intermediate-ink);
|
||||
}
|
||||
.badge-advanced {
|
||||
color: var(--diff-advanced-ink);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
</script>
|
||||
|
||||
<!-- Marqueur de fin explicite : restitue le point d'arrêt naturel que le
|
||||
scroll infini supprime (doctrine §1.5 et §3.2). -->
|
||||
<p class="end" role="status">
|
||||
<strong>{t('directory.endOfList')}</strong>
|
||||
<span>{t('directory.endOfListSub')}</span>
|
||||
</p>
|
||||
|
||||
<style>
|
||||
.end {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
text-align: center;
|
||||
padding-block: var(--space-5);
|
||||
border-top: 4px double var(--oki-line);
|
||||
margin-block-start: var(--space-5);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2);
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { getLocale, setLocale, locales, localeNames, t } from '$lib/i18n/index.svelte'
|
||||
</script>
|
||||
|
||||
<!-- Bascule explicite FR / Kréyòl, persistée en localStorage (mission §4.3).
|
||||
Amélioration progressive : sans JS, le site reste lisible en français. -->
|
||||
<div class="switch" role="group" aria-label={t('lang.switchLabel')}>
|
||||
{#each locales as locale (locale)}
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={getLocale() === locale}
|
||||
onclick={() => setLocale(locale)}
|
||||
>
|
||||
{localeNames[locale]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
display: inline-flex;
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
min-height: var(--tap-target);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
button[aria-pressed='true'] {
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
// Technologie périphérique (doctrine §3.5) : l'état réseau est une
|
||||
// information non urgente — un badge discret, jamais une interruption.
|
||||
let offline = $state(browser ? !navigator.onLine : false)
|
||||
</script>
|
||||
|
||||
<svelte:window ononline={() => (offline = false)} onoffline={() => (offline = true)} />
|
||||
|
||||
{#if offline}
|
||||
<p class="offline" role="status">
|
||||
<strong>{t('offline.badge')}</strong> — {t('offline.detail')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.offline {
|
||||
font-size: var(--text-0);
|
||||
color: var(--oki-ink-2);
|
||||
background: var(--oki-surface-2);
|
||||
border-block: 1px solid var(--oki-line);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { value = $bindable('') }: { value?: string } = $props()
|
||||
let input: HTMLInputElement
|
||||
|
||||
// raccourci « / » desktop (doctrine §2.2 — le clavier est roi)
|
||||
function onkeydown(e: KeyboardEvent) {
|
||||
if (e.key === '/' && document.activeElement !== input) {
|
||||
const target = e.target
|
||||
if (target instanceof HTMLElement && target.closest('input, textarea, [contenteditable]'))
|
||||
return
|
||||
e.preventDefault()
|
||||
input.focus()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window {onkeydown} />
|
||||
|
||||
<p class="search">
|
||||
<label>
|
||||
<span class="visually-hidden">{t('search.label')}</span>
|
||||
<input
|
||||
bind:this={input}
|
||||
bind:value
|
||||
type="search"
|
||||
placeholder={t('search.placeholder')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<style>
|
||||
input {
|
||||
inline-size: 100%;
|
||||
max-inline-size: 28rem;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--oki-surface);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
// Le clavier est roi sur desktop (doctrine §2.2.3) : raccourcis découvrables
|
||||
// via « ? », jamais porteurs d'information exclusive (tout est aussi dans la nav).
|
||||
let dialog: HTMLDialogElement
|
||||
|
||||
const KEYS = [
|
||||
{ key: '/', label: 'shortcuts.search' },
|
||||
{ key: 'h', label: 'shortcuts.home' },
|
||||
{ key: 'a', label: 'shortcuts.directory' },
|
||||
{ key: '?', label: 'shortcuts.help' },
|
||||
{ key: 'Échap', label: 'shortcuts.close' }
|
||||
]
|
||||
|
||||
function onkeydown(e: KeyboardEvent) {
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) return
|
||||
const target = e.target
|
||||
if (target instanceof HTMLElement && target.closest('input, textarea, select, [contenteditable]'))
|
||||
return
|
||||
if (e.key === '?') {
|
||||
e.preventDefault()
|
||||
dialog.open ? dialog.close() : dialog.showModal()
|
||||
} else if (e.key === 'h') {
|
||||
goto('/')
|
||||
} else if (e.key === 'a') {
|
||||
goto('/outils')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window {onkeydown} />
|
||||
|
||||
<!-- <dialog> natif : focus piégé, Échap géré par le navigateur, zéro ARIA maison -->
|
||||
<dialog bind:this={dialog}>
|
||||
<h2>{t('shortcuts.title')}</h2>
|
||||
<dl>
|
||||
{#each KEYS as { key, label } (key)}
|
||||
<div>
|
||||
<dt><kbd>{key}</kbd></dt>
|
||||
<dd>{t(label)}</dd>
|
||||
</div>
|
||||
{/each}
|
||||
</dl>
|
||||
<form method="dialog">
|
||||
<button>{t('shortcuts.close')}</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
dialog {
|
||||
border: 1px solid var(--oki-line);
|
||||
border-top: 4px solid var(--oki-jaune);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--oki-surface);
|
||||
color: var(--oki-ink);
|
||||
padding: var(--space-4);
|
||||
max-inline-size: 24rem;
|
||||
}
|
||||
|
||||
dialog::backdrop {
|
||||
background: oklch(0% 0 0 / 0.5);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--text-2);
|
||||
margin-block-end: var(--space-3);
|
||||
}
|
||||
|
||||
dl {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
margin-block-end: var(--space-4);
|
||||
}
|
||||
|
||||
dl div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
min-inline-size: 2rem;
|
||||
padding: 0.15em 0.5em;
|
||||
border: 1px solid var(--oki-line);
|
||||
border-bottom-width: 3px;
|
||||
border-radius: var(--radius-1);
|
||||
background: var(--oki-surface-2);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<script lang="ts">
|
||||
import DifficultyBadge from './DifficultyBadge.svelte'
|
||||
import { isAdopted } from '$lib/adopted.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
interface CardTool {
|
||||
name: string
|
||||
slug: string
|
||||
difficulty: 'beginner' | 'intermediate' | 'advanced'
|
||||
license?: string
|
||||
color?: string
|
||||
monogram?: string
|
||||
tags: string[]
|
||||
excerpt: string
|
||||
}
|
||||
|
||||
let { tool }: { tool: CardTool } = $props()
|
||||
</script>
|
||||
|
||||
<article class="card">
|
||||
<header class="cluster">
|
||||
<span class="monogram" style:--tool-color={tool.color} aria-hidden="true">
|
||||
{tool.monogram ?? tool.name.slice(0, 2)}
|
||||
</span>
|
||||
<h3><a href="/outils/{tool.slug}">{tool.name}</a></h3>
|
||||
{#if isAdopted(tool.slug)}
|
||||
<!-- information périphérique (doctrine §3.5) : discret, jamais bloquant -->
|
||||
<span class="adopted" title={t('adopted.marked')}>
|
||||
<span aria-hidden="true">✓</span>
|
||||
<span class="visually-hidden">{t('adopted.marked')}</span>
|
||||
</span>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
<p class="excerpt"><span class="visually-hidden">{t('tool.why')} : </span>{tool.excerpt}</p>
|
||||
|
||||
<footer class="cluster">
|
||||
<DifficultyBadge difficulty={tool.difficulty} />
|
||||
{#if tool.license}
|
||||
<span class="meta">{tool.license}</span>
|
||||
{/if}
|
||||
{#each tool.tags as tag (tag)}
|
||||
<span class="meta">{tag}</span>
|
||||
{/each}
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-left: 4px solid var(--oki-jaune);
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--oki-surface);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* la couleur de marque en anneau, jamais en fond de texte :
|
||||
contraste AA garanti quel que soit l'outil (doctrine §4.3) */
|
||||
.monogram {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
inline-size: 2.5rem;
|
||||
block-size: 2.5rem;
|
||||
border-radius: var(--radius-2);
|
||||
background: var(--oki-surface-2);
|
||||
border: 3px solid var(--tool-color, var(--oki-line));
|
||||
color: var(--oki-ink);
|
||||
font-weight: 700;
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--text-2);
|
||||
}
|
||||
|
||||
/* toute la carte est cliquable via le pseudo-élément du lien —
|
||||
un vrai <a>, pas de div cliquable (doctrine §4.3) */
|
||||
h3 a {
|
||||
text-decoration: none;
|
||||
}
|
||||
h3 a::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
h3 a:focus-visible::after {
|
||||
outline: var(--focus-ring);
|
||||
outline-offset: -3px;
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
.adopted {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
inline-size: 1.4rem;
|
||||
block-size: 1.4rem;
|
||||
border-radius: 50%;
|
||||
background: var(--oki-vert);
|
||||
color: oklch(98% 0 0);
|
||||
font-size: var(--text-0);
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.excerpt {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: var(--text-0);
|
||||
color: var(--oki-ink-2);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 0.1em 0.6em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Chargeur de contenu — s'exécute uniquement au build (site prérendu,
|
||||
* ADR-001) : ni fs ni marked ne sont expédiés au client.
|
||||
*/
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { marked } from 'marked'
|
||||
|
||||
export type Difficulty = 'beginner' | 'intermediate' | 'advanced'
|
||||
|
||||
export interface ToolSections {
|
||||
what?: string
|
||||
why?: string
|
||||
who?: string
|
||||
install?: string
|
||||
extra?: string
|
||||
}
|
||||
|
||||
export interface Tool {
|
||||
name: string
|
||||
slug: string
|
||||
category: string
|
||||
difficulty: Difficulty
|
||||
license?: string
|
||||
url: string
|
||||
color?: string
|
||||
monogram?: string
|
||||
iconSlug?: string
|
||||
tags: string[]
|
||||
/** extrait texte brut de « why », pour les cartes */
|
||||
excerpt: string
|
||||
sections: ToolSections
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
slug: string
|
||||
title: string
|
||||
order: number
|
||||
part: number
|
||||
}
|
||||
|
||||
const CONTENT = path.resolve(process.cwd(), '..', 'content')
|
||||
const SECTION_KEYS = ['what', 'why', 'who', 'install', 'extra'] as const
|
||||
|
||||
function parseFrontmatter(raw: string): [Record<string, unknown>, string] {
|
||||
const m = raw.match(/^---\n([\s\S]*?)\n---\n?/)
|
||||
if (!m) throw new Error('frontmatter manquant')
|
||||
const meta: Record<string, unknown> = {}
|
||||
for (const line of m[1].split('\n')) {
|
||||
const i = line.indexOf(':')
|
||||
if (i === -1) continue
|
||||
// les valeurs sont encodées en JSON par le script de migration
|
||||
meta[line.slice(0, i).trim()] = JSON.parse(line.slice(i + 1).trim())
|
||||
}
|
||||
return [meta, raw.slice(m[0].length)]
|
||||
}
|
||||
|
||||
function parseSections(body: string): ToolSections {
|
||||
const sections: ToolSections = {}
|
||||
const parts = body.split(/^## (\w+)\s*$/m)
|
||||
for (let i = 1; i < parts.length; i += 2) {
|
||||
const key = parts[i] as (typeof SECTION_KEYS)[number]
|
||||
if (SECTION_KEYS.includes(key)) {
|
||||
sections[key] = marked.parse(parts[i + 1].trim(), { async: false })
|
||||
}
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
const ENTITIES: Record<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
''': "'",
|
||||
' ': ' '
|
||||
}
|
||||
|
||||
function stripHtml(html: string): string {
|
||||
return html
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (e) => ENTITIES[e])
|
||||
.replace(/[ \t\r\n]+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function loadTool(file: string): Tool {
|
||||
const raw = readFileSync(path.join(CONTENT, 'outils', file), 'utf8')
|
||||
const [meta, body] = parseFrontmatter(raw)
|
||||
const sections = parseSections(body)
|
||||
const why = stripHtml(sections.why ?? sections.what ?? '')
|
||||
return {
|
||||
...(meta as unknown as Omit<Tool, 'sections' | 'excerpt'>),
|
||||
excerpt: why.length > 160 ? `${why.slice(0, 157).trimEnd()}…` : why,
|
||||
sections
|
||||
}
|
||||
}
|
||||
|
||||
export interface GcfTool {
|
||||
sections: ToolSections
|
||||
excerpt: string
|
||||
}
|
||||
|
||||
/** Fiches jumelles créoles : content/outils/gcf/<slug>.md (CONTENT_GUIDE). */
|
||||
function loadGcf(): Record<string, GcfTool> {
|
||||
const dir = path.join(CONTENT, 'outils', 'gcf')
|
||||
if (!existsSync(dir)) return {}
|
||||
const out: Record<string, GcfTool> = {}
|
||||
for (const file of readdirSync(dir).filter((f) => f.endsWith('.md'))) {
|
||||
const raw = readFileSync(path.join(dir, file), 'utf8')
|
||||
const [meta, body] = parseFrontmatter(raw)
|
||||
const sections = parseSections(body)
|
||||
const why = stripHtml(sections.why ?? sections.what ?? '')
|
||||
out[(meta.slug as string) ?? file.replace(/\.md$/, '')] = {
|
||||
excerpt: why.length > 160 ? `${why.slice(0, 157).trimEnd()}…` : why,
|
||||
sections
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
let cache: { tools: Tool[]; categories: Category[]; gcf: Record<string, GcfTool> } | null = null
|
||||
|
||||
export function loadContent() {
|
||||
if (cache) return cache
|
||||
const categories = (
|
||||
JSON.parse(readFileSync(path.join(CONTENT, 'categories.json'), 'utf8')) as Category[]
|
||||
).sort((a, b) => a.order - b.order)
|
||||
const order = new Map(categories.map((c, i) => [c.slug, i]))
|
||||
const tools = readdirSync(path.join(CONTENT, 'outils'))
|
||||
.filter((f) => f.endsWith('.md'))
|
||||
.map(loadTool)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(order.get(a.category) ?? 99) - (order.get(b.category) ?? 99) ||
|
||||
a.name.localeCompare(b.name, 'fr')
|
||||
)
|
||||
cache = { tools, categories, gcf: loadGcf() }
|
||||
return cache
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"site": {
|
||||
"name": "Exit Chat Control",
|
||||
"tagline": "Reprends le contrôle de ton internet",
|
||||
"description": "Annuaire d'outils libres pour la souveraineté numérique — messagerie chiffrée, VPN, auto-hébergement, OS libres."
|
||||
},
|
||||
"nav": {
|
||||
"home": "Accueil",
|
||||
"directory": "Annuaire",
|
||||
"contribute": "Contribuer",
|
||||
"skipToContent": "Aller au contenu"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Chwazi zouti ou",
|
||||
"subtitle": "Reprends le contrôle de ton internet : des outils libres, expliqués simplement, du premier geste au serveur auto-hébergé.",
|
||||
"cta": "Voir l'annuaire"
|
||||
},
|
||||
"intro": {
|
||||
"title": "Pourquoi ce site ?",
|
||||
"p1": "L'Union européenne veut imposer le scan de toutes tes conversations privées : c'est le projet « Chat Control ». Et même quand la loi recule, l'infrastructure de surveillance, elle, continue d'avancer.",
|
||||
"p2": "Ce site est un guide de terrain. Des outils libres, vérifiés, expliqués pas à pas — pour sortir des plateformes qui te surveillent et reprendre la main : toi, ta famille, ton lakou.",
|
||||
"p3": "Pas besoin d'être technicien·ne. Commence par les fondations, avance à ton rythme, le reste vient après."
|
||||
},
|
||||
"sommaire": {
|
||||
"title": "Le chimen",
|
||||
"sub": "Trois étapes, du premier geste à l'autodéfense complète. Chaque catégorie est une porte d'entrée."
|
||||
},
|
||||
"arc": {
|
||||
"fondations": "Les fondations",
|
||||
"controle": "Reprendre le contrôle",
|
||||
"avance": "Autodéfense avancée"
|
||||
},
|
||||
"directory": {
|
||||
"title": "Annuaire des outils",
|
||||
"intro": "Chaque outil est libre, vérifié, et classé par difficulté. Commence par le vert.",
|
||||
"filterCategory": "Catégorie",
|
||||
"filterDifficulty": "Difficulté",
|
||||
"allCategories": "Toutes les catégories",
|
||||
"allDifficulties": "Toutes",
|
||||
"results": "outils",
|
||||
"resultOne": "outil",
|
||||
"empty": "Ayen pa maché èvè filtè-lasa — essaie d'élargir les filtres.",
|
||||
"endOfList": "Ou pa manké ayen",
|
||||
"endOfListSub": "Tu es au bout de la liste — rien ne se recharge dans ton dos.",
|
||||
"prevPage": "Page précédente",
|
||||
"nextPage": "Page suivante",
|
||||
"page": "Page"
|
||||
},
|
||||
"tool": {
|
||||
"what": "À quoi ça sert",
|
||||
"why": "Pourquoi c'est important",
|
||||
"who": "Pour qui, quand",
|
||||
"install": "Installer et utiliser",
|
||||
"officialSite": "Site officiel",
|
||||
"sourceCode": "Code source",
|
||||
"license": "Licence",
|
||||
"category": "Catégorie",
|
||||
"backToDirectory": "Retour à l'annuaire",
|
||||
"notTranslated": "Fich-lasa póko tradui an kréyòl — la voici en français.",
|
||||
"draftTranslation": "Tradiksyon kréyòl an chantyé — un doute ? La version française fait foi."
|
||||
},
|
||||
"adopted": {
|
||||
"mark": "Je m'en sers",
|
||||
"marked": "Je m'en sers ✓",
|
||||
"progressBefore": "Ou ja fè",
|
||||
"progressAfter": "gestes — chaque outil adopté est une reprise de terrain.",
|
||||
"srHint": "Enregistré uniquement sur cet appareil, jamais envoyé nulle part."
|
||||
},
|
||||
"difficulty": {
|
||||
"beginner": "Débutant",
|
||||
"intermediate": "Intermédiaire",
|
||||
"advanced": "Avancé"
|
||||
},
|
||||
"search": {
|
||||
"label": "Rechercher un outil",
|
||||
"placeholder": "Rechercher… (touche /)",
|
||||
"noResults": "Aucun outil trouvé"
|
||||
},
|
||||
"lang": {
|
||||
"switchLabel": "Langue",
|
||||
"switchTo": "Passer en"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Raccourcis clavier",
|
||||
"search": "Rechercher",
|
||||
"help": "Afficher cette aide",
|
||||
"home": "Aller à l'accueil",
|
||||
"directory": "Aller à l'annuaire",
|
||||
"close": "Fermer"
|
||||
},
|
||||
"offline": {
|
||||
"badge": "Hors-ligne",
|
||||
"detail": "Tu lis la copie locale — les fiches déjà visitées restent disponibles."
|
||||
},
|
||||
"contrib": {
|
||||
"title": "Pòté mannèv — Contribuer",
|
||||
"intro": "Ce guide vit par celles et ceux qui s'en servent. Trois façons d'aider, de la plus simple à la plus complète.",
|
||||
"suggest": "Proposer un outil",
|
||||
"suggestBody": "Tu utilises un outil libre qui manque à l'annuaire ? Ouvre une demande avec le formulaire : nom, lien, pourquoi il compte. On s'occupe de la fiche.",
|
||||
"suggestCta": "Ouvrir une demande",
|
||||
"fix": "Corriger une fiche",
|
||||
"fixBody": "Une étape d'installation qui a changé, un lien mort, une tournure plus claire ? Chaque fiche est un simple fichier texte dans le dossier content/outils/.",
|
||||
"fixCta": "Parcourir les fiches",
|
||||
"translate": "Traduire en kréyòl",
|
||||
"translateBody": "Le créole est la langue du cœur de ce site. Les fiches attendent leur jumelle gcf — et les traductions existantes ont besoin de relecteurs et relectrices natifs.",
|
||||
"translateCta": "Lire le guide de contenu",
|
||||
"repo": "Tout se passe sur la forge",
|
||||
"repoBody": "Le code, le contenu et les discussions vivent sur labola, la forge auto-hébergée d'OKI. Pas de compte Microsoft requis.",
|
||||
"repoCta": "Voir le dépôt"
|
||||
},
|
||||
"footer": {
|
||||
"license": "Contenu et code libres (MIT). Zéro tracker, zéro CDN, zéro cookie.",
|
||||
"contribute": "Proposer un outil ou une correction"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"site": {
|
||||
"name": "Exit Chat Control",
|
||||
"tagline": "Pran kontwòl entènèt a-w",
|
||||
"description": "Lis a zouti lib pou libèté niméwik — mésajri chifré, VPN, oto-ébèjman, sistèm lib."
|
||||
},
|
||||
"nav": {
|
||||
"home": "Lakou",
|
||||
"directory": "Lis a zouti",
|
||||
"contribute": "Pòté mannèv",
|
||||
"skipToContent": "Alé dirèk asi kontni-la"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Chwazi zouti ou",
|
||||
"subtitle": "Pran kontwòl entènèt a-w : zouti lib, èspliké senp, dépi prèmyé jès jis sèwvè a-w.",
|
||||
"cta": "Gadé lis-la"
|
||||
},
|
||||
"intro": {
|
||||
"title": "Poukisa sit-lasa ?",
|
||||
"p1": "Linyon éwopéyèn vlé enposé skannaj a tout konvèsasyon privé a-w : sé pwojé « Chat Control ». É menm lè lwa-la ka fè bak, enfrastrikti a survéyans-la ka kontinyé vansé.",
|
||||
"p2": "Sit-lasa sé on gid a téren. Zouti lib, vérifyé, èspliké pa a pa — pou sòti an platfòm ki ka survéyé-w é répran lanmen : vou, fanmi a-w, lakou a-w.",
|
||||
"p3": "Pa bizwen sav enfòwmatik. Koumansé èvè fondasyon-la, vansé a vitès a-w, rès-la ka vin apré."
|
||||
},
|
||||
"sommaire": {
|
||||
"title": "Chimen-la",
|
||||
"sub": "Twa létap, dépi prèmyé jès jis otodéfans konplé. Chak katégori sé on pòt."
|
||||
},
|
||||
"arc": {
|
||||
"fondations": "Fondasyon-la",
|
||||
"controle": "Répran kontwòl",
|
||||
"avance": "Otodéfans avansé"
|
||||
},
|
||||
"directory": {
|
||||
"title": "Lis a zouti",
|
||||
"intro": "Chak zouti lib, vérifyé, é rangé pa difikilté. Koumansé èvè vèw-la.",
|
||||
"filterCategory": "Katégori",
|
||||
"filterDifficulty": "Difikilté",
|
||||
"allCategories": "Tout katégori",
|
||||
"allDifficulties": "Tout",
|
||||
"results": "zouti",
|
||||
"resultOne": "zouti",
|
||||
"empty": "Ayen pa maché èvè filtè-lasa — èséyé louvri filtè a-w.",
|
||||
"endOfList": "Ou pa manké ayen",
|
||||
"endOfListSub": "Ou rivé an bout a lis-la — ayen pa ka richajé dèyè do a-w.",
|
||||
"prevPage": "Paj avan",
|
||||
"nextPage": "Paj apré",
|
||||
"page": "Paj"
|
||||
},
|
||||
"tool": {
|
||||
"what": "Pou ki sa i sèwvi",
|
||||
"why": "Poukisa sa enpòwtan",
|
||||
"who": "Pou kimoun, kitan",
|
||||
"install": "Enstalé é sèwvi",
|
||||
"officialSite": "Sit ofisyèl",
|
||||
"sourceCode": "Kòd sous",
|
||||
"license": "Lisans",
|
||||
"category": "Katégori",
|
||||
"backToDirectory": "Viré an lis-la",
|
||||
"notTranslated": "Fich-lasa póko tradui an kréyòl — mi'y an fransé.",
|
||||
"draftTranslation": "Tradiksyon kréyòl an chantyé — on dout ? Vèwsyon fransé-la ka fè fwa."
|
||||
},
|
||||
"adopted": {
|
||||
"mark": "An ka sèvi épi'y",
|
||||
"marked": "An ka sèvi épi'y ✓",
|
||||
"progressBefore": "Ou ja fè",
|
||||
"progressAfter": "jès — chak zouti adopté sé on moso téren ou répran.",
|
||||
"srHint": "Anrèjistré asi apawèy-lasa sèlman, janmé voyé okenn koté."
|
||||
},
|
||||
"difficulty": {
|
||||
"beginner": "Débitan",
|
||||
"intermediate": "Entèmédia",
|
||||
"advanced": "Avansé"
|
||||
},
|
||||
"search": {
|
||||
"label": "Chèché on zouti",
|
||||
"placeholder": "Chèché… (touch /)",
|
||||
"noResults": "Pon zouti pa touvé"
|
||||
},
|
||||
"lang": {
|
||||
"switchLabel": "Lang",
|
||||
"switchTo": "Pasé an"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Rakousi klavyé",
|
||||
"search": "Chèché",
|
||||
"help": "Montré èd-lasa",
|
||||
"home": "Alé an lakou-la",
|
||||
"directory": "Alé an lis-la",
|
||||
"close": "Fèmé"
|
||||
},
|
||||
"offline": {
|
||||
"badge": "Dékonèkté",
|
||||
"detail": "Ou ka li kopi lokal-la — fich ou ja vizité ka rété la."
|
||||
},
|
||||
"contrib": {
|
||||
"title": "Pòté mannèv",
|
||||
"intro": "Gid-lasa ka viv gras a moun ki ka sèvi épi'y. Twa mannyè pou édé, dépi pi senp jis pi konplé.",
|
||||
"suggest": "Pwopozé on zouti",
|
||||
"suggestBody": "Ou ka sèvi épi on zouti lib ki manké an lis-la ? Louvè on dimann èvè fòwmilè-la : non, lyen, poukisa i konté. Nou ka okipé fich-la.",
|
||||
"suggestCta": "Louvè on dimann",
|
||||
"fix": "Korijé on fich",
|
||||
"fixBody": "On létap ki chanjé, on lyen mò, on fraz pi klè ? Chak fich sé on senp fichyé tèks an dosyé content/outils/.",
|
||||
"fixCta": "Gadé fich-la yo",
|
||||
"translate": "Tradui an kréyòl",
|
||||
"translateBody": "Kréyòl sé lang a kè a sit-lasa. Fich-la yo ka atann jimèl gcf a-yo — é tradiksyon ki ja la bizwen lektè natif.",
|
||||
"translateCta": "Li gid a kontni-la",
|
||||
"repo": "Tout ka pasé asi fòwj-la",
|
||||
"repoBody": "Kòd, kontni é diskisyon ka viv asi labola, fòwj oto-ébèjé a OKI. Pa bizwen kont Microsoft.",
|
||||
"repoCta": "Gadé dépo-la"
|
||||
},
|
||||
"footer": {
|
||||
"license": "Kontni é kòd lib (MIT). Pon trakè, pon CDN, pon cookie.",
|
||||
"contribute": "Pwopozé on zouti oben on koreksyon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import fr from './fr.json'
|
||||
import gcf from './gcf.json'
|
||||
import { browser } from '$app/environment'
|
||||
import { defaultLocale, type Locale } from './locales'
|
||||
|
||||
export { locales, localeNames, defaultLocale, type Locale } from './locales'
|
||||
|
||||
type Dict = typeof fr
|
||||
// gcf est vérifié par TypeScript contre la forme du français : une clé
|
||||
// manquante est une erreur de build, pas un fallback silencieux (ADR-001).
|
||||
const dicts: Record<Locale, Dict> = { fr, gcf }
|
||||
|
||||
const STORAGE_KEY = 'oki-lang'
|
||||
|
||||
function initialLocale(): Locale {
|
||||
if (!browser) return defaultLocale
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
return saved === 'gcf' || saved === 'fr' ? saved : defaultLocale
|
||||
}
|
||||
|
||||
// Rune d'état partagée (Svelte 5) : la langue de l'interface.
|
||||
// Le HTML prérendu est en fr ; le créole est une amélioration progressive
|
||||
// (persistance localStorage — mission §4.3, doctrine §4.2).
|
||||
const state = $state({ locale: initialLocale() })
|
||||
|
||||
export function getLocale(): Locale {
|
||||
return state.locale
|
||||
}
|
||||
|
||||
export function setLocale(locale: Locale) {
|
||||
state.locale = locale
|
||||
if (browser) {
|
||||
localStorage.setItem(STORAGE_KEY, locale)
|
||||
document.documentElement.lang = locale
|
||||
}
|
||||
}
|
||||
|
||||
function lookup(dict: Dict, path: string): unknown {
|
||||
return path.split('.').reduce<unknown>((node, key) => {
|
||||
if (node && typeof node === 'object' && key in node) {
|
||||
return (node as Record<string, unknown>)[key]
|
||||
}
|
||||
return undefined
|
||||
}, dict)
|
||||
}
|
||||
|
||||
/** Chaîne d'interface pour `path` dans la langue courante (réactif). */
|
||||
export function t(path: string): string {
|
||||
const hit = lookup(dicts[state.locale], path) ?? lookup(dicts[defaultLocale], path)
|
||||
if (typeof hit !== 'string') {
|
||||
throw new Error(`Clé i18n manquante « ${path} » (locale : ${state.locale})`)
|
||||
}
|
||||
return hit
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// gcf = créole guadeloupéen (ISO 639-3, valide BCP 47).
|
||||
// La langue du cœur d'OKI : les micro-textes créoles activent les circuits
|
||||
// de confiance avant toute lecture consciente (doctrine §4.2).
|
||||
export const locales = ['fr', 'gcf'] as const
|
||||
export type Locale = (typeof locales)[number]
|
||||
export const defaultLocale: Locale = 'fr'
|
||||
|
||||
export const localeNames: Record<Locale, string> = {
|
||||
fr: 'Français',
|
||||
gcf: 'Kréyòl'
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/** 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 NEW_TOOL_ISSUE_URL = `${REPO_URL}/issues/new?template=.gitea/ISSUE_TEMPLATE/nouvo-zouti.yaml`
|
||||
|
||||
/**
|
||||
* Les trois arcs du chimen (sommaire) : progression Fondations →
|
||||
* Reprendre le contrôle → Autodéfense avancée. Une catégorie appartient
|
||||
* au premier arc dont `maxPart` couvre son `part`.
|
||||
*/
|
||||
export const ARCS = [
|
||||
{ id: 'fondations', maxPart: 4, difficulty: 'beginner' },
|
||||
{ id: 'controle', maxPart: 9, difficulty: 'intermediate' },
|
||||
{ id: 'avance', maxPart: 99, difficulty: 'advanced' }
|
||||
] as const
|
||||
|
||||
export type ArcId = (typeof ARCS)[number]['id']
|
||||
|
||||
export function arcOf(part: number): ArcId {
|
||||
return (ARCS.find((a) => part <= a.maxPart) ?? ARCS[ARCS.length - 1]).id
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import '../app.css'
|
||||
import { onNavigate } from '$app/navigation'
|
||||
import LanguageSwitcher from '$lib/components/LanguageSwitcher.svelte'
|
||||
import OfflineBadge from '$lib/components/OfflineBadge.svelte'
|
||||
import ShortcutsDialog from '$lib/components/ShortcutsDialog.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
import { REPO_URL } from '$lib/site'
|
||||
|
||||
let { children } = $props()
|
||||
|
||||
// View Transitions : la navigation glisse au lieu de sauter — amélioration
|
||||
// progressive pure (navigateurs sans support : rien ne change), et
|
||||
// désactivée si la personne préfère réduire les animations (doctrine §4.3).
|
||||
onNavigate((navigation) => {
|
||||
if (!document.startViewTransition) return
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||
return new Promise((resolve) => {
|
||||
document.startViewTransition(async () => {
|
||||
resolve()
|
||||
await navigation.complete
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<a class="skip-link" href="#main">{t('nav.skipToContent')}</a>
|
||||
|
||||
<header class="center">
|
||||
<div class="cluster bar">
|
||||
<a class="brand" href="/">
|
||||
<span class="brand-mark" aria-hidden="true"></span>
|
||||
{t('site.name')}
|
||||
</a>
|
||||
<nav aria-label={t('nav.home')}>
|
||||
<ul class="cluster">
|
||||
<li><a href="/outils">{t('nav.directory')}</a></li>
|
||||
<li><a href="/contribuer">{t('nav.contribute')}</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<OfflineBadge />
|
||||
<ShortcutsDialog />
|
||||
|
||||
<main id="main" class="center stack" style="--gap: var(--space-5)">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<footer class="center">
|
||||
<p>{t('footer.license')}</p>
|
||||
<p>
|
||||
<a href={REPO_URL} rel="external">{t('footer.contribute')}</a>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.bar {
|
||||
justify-content: space-between;
|
||||
padding-block: var(--space-3);
|
||||
border-bottom: 4px solid var(--oki-jaune);
|
||||
margin-block-end: var(--space-4);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
font-size: var(--text-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
min-height: var(--tap-target);
|
||||
}
|
||||
|
||||
/* marque OKI : les trois bandes panafricaines — identité du territoire
|
||||
dès le premier écran, test des 50 ms (doctrine §1.1 et §4.2) */
|
||||
.brand-mark {
|
||||
inline-size: 0.9rem;
|
||||
block-size: 1.6rem;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--oki-vert) 0 33%,
|
||||
var(--oki-jaune) 33% 66%,
|
||||
var(--oki-rouge) 66% 100%
|
||||
);
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--oki-jaune);
|
||||
text-decoration-thickness: 3px;
|
||||
}
|
||||
|
||||
main {
|
||||
padding-block-end: var(--space-6);
|
||||
}
|
||||
|
||||
footer {
|
||||
border-top: 1px solid var(--oki-line);
|
||||
padding-block: var(--space-4);
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,3 @@
|
||||
// Tout le site est prérendu (ADR-001) : HTML statique servi par nginx,
|
||||
// un lien fonctionne sans JS (doctrine §4.4).
|
||||
export const prerender = true
|
||||
@@ -0,0 +1,27 @@
|
||||
import { loadContent } from '$lib/content/tools.server'
|
||||
import { ARCS, arcOf } from '$lib/site'
|
||||
|
||||
export function load() {
|
||||
const { tools, categories } = loadContent()
|
||||
const countBy = new Map<string, number>()
|
||||
for (const tool of tools) {
|
||||
countBy.set(tool.category, (countBy.get(tool.category) ?? 0) + 1)
|
||||
}
|
||||
// le sommaire : catégories groupées par arc (Fondations → Contrôle → Avancé)
|
||||
const arcs = ARCS.map((arc) => ({
|
||||
id: arc.id,
|
||||
difficulty: arc.difficulty,
|
||||
categories: categories
|
||||
.filter((c) => arcOf(c.part) === arc.id)
|
||||
.map((c) => ({ ...c, count: countBy.get(c.slug) ?? 0 }))
|
||||
}))
|
||||
return {
|
||||
arcs,
|
||||
toolCount: tools.length,
|
||||
// les premiers gestes : outils débutant mis en avant
|
||||
starters: tools
|
||||
.filter((t) => t.difficulty === 'beginner')
|
||||
.slice(0, 3)
|
||||
.map(({ sections: _sections, ...card }) => card)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
<script lang="ts">
|
||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||
import { adoptedCount } from '$lib/adopted.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { data } = $props()
|
||||
// n'apparaît qu'après le premier geste : jamais de « 0/55 » culpabilisant
|
||||
// (doctrine §3.3 — pas de métrique anxiogène)
|
||||
let jes = $derived(adoptedCount())
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<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,
|
||||
et donne UNE action primaire (Von Restorff, doctrine §1.2). -->
|
||||
<section class="hero stack">
|
||||
<h1>{t('hero.title')}</h1>
|
||||
<p class="sub">{t('hero.subtitle')}</p>
|
||||
<p><a class="cta" href="/outils">{t('hero.cta')} · {data.toolCount} {t('directory.results')}</a></p>
|
||||
{#if jes > 0}
|
||||
<p class="jes" role="status">
|
||||
<span class="jes-mark" aria-hidden="true">✓</span>
|
||||
{t('adopted.progressBefore')}
|
||||
<strong>{jes}</strong>
|
||||
{t('adopted.progressAfter')}
|
||||
</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- L'intro publique : de quoi il s'agit, avant toute navigation.
|
||||
Motif en F (doctrine §1.1) : l'essentiel au premier mot de chaque ¶. -->
|
||||
<section class="intro stack prose">
|
||||
<h2>{t('intro.title')}</h2>
|
||||
<p>{t('intro.p1')}</p>
|
||||
<p>{t('intro.p2')}</p>
|
||||
<p>{t('intro.p3')}</p>
|
||||
</section>
|
||||
|
||||
<!-- Le sommaire-chimen : trois arcs, une progression lisible sans lecture
|
||||
(reconnaissance, doctrine §1.2) — le rail reprend vert/jaune/rouge,
|
||||
doublé par texte + numérotation (jamais couleur seule, §1.1). -->
|
||||
<section class="stack" aria-labelledby="sommaire-title">
|
||||
<h2 id="sommaire-title">{t('sommaire.title')}</h2>
|
||||
<p class="sub2">{t('sommaire.sub')}</p>
|
||||
<ol class="chimen">
|
||||
{#each data.arcs as arc, i (arc.id)}
|
||||
<li class="arc arc-{arc.difficulty}">
|
||||
<h3>
|
||||
<span class="arc-num" aria-hidden="true">{i + 1}</span>
|
||||
{t(`arc.${arc.id}`)}
|
||||
<span class="arc-diff">{t(`difficulty.${arc.difficulty}`)}</span>
|
||||
</h3>
|
||||
<ul class="cluster">
|
||||
{#each arc.categories as cat (cat.slug)}
|
||||
<li>
|
||||
<a href="/outils?cat={cat.slug}">
|
||||
{cat.title}
|
||||
<span class="count">{cat.count}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section class="stack">
|
||||
<h2>{t('directory.intro')}</h2>
|
||||
<div class="card-grid">
|
||||
<ul>
|
||||
{#each data.starters as tool (tool.slug)}
|
||||
<li><ToolCard {tool} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
padding-block: var(--space-5);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: var(--text-2);
|
||||
color: var(--oki-ink-2);
|
||||
max-width: var(--measure);
|
||||
}
|
||||
|
||||
.sub2 {
|
||||
color: var(--oki-ink-2);
|
||||
}
|
||||
|
||||
/* une seule action primaire par écran (Von Restorff, doctrine §1.2) */
|
||||
.cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-4);
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-weight: 700;
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cta:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
.jes {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
max-width: var(--measure);
|
||||
}
|
||||
|
||||
.jes-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
inline-size: 1.4rem;
|
||||
block-size: 1.4rem;
|
||||
border-radius: 50%;
|
||||
background: var(--oki-vert);
|
||||
color: oklch(98% 0 0);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.intro {
|
||||
border-inline-start: 4px solid var(--oki-rouge);
|
||||
padding-inline-start: var(--space-4);
|
||||
}
|
||||
|
||||
/* ── le chimen ─────────────────────────────────────────────────── */
|
||||
.chimen {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.arc {
|
||||
--rail: var(--oki-line);
|
||||
position: relative;
|
||||
padding-inline-start: var(--space-5);
|
||||
padding-block-end: var(--space-5);
|
||||
}
|
||||
|
||||
/* le rail vertical, coloré par arc */
|
||||
.arc::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-block: 0.4rem 0;
|
||||
inset-inline-start: calc(1.1rem - 2px);
|
||||
width: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--rail);
|
||||
}
|
||||
|
||||
.arc:last-child::before {
|
||||
inset-block-end: auto;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.arc-beginner {
|
||||
--rail: var(--diff-beginner);
|
||||
}
|
||||
.arc-intermediate {
|
||||
--rail: var(--diff-intermediate);
|
||||
}
|
||||
.arc-advanced {
|
||||
--rail: var(--diff-advanced);
|
||||
}
|
||||
|
||||
.arc h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-block-end: var(--space-3);
|
||||
}
|
||||
|
||||
.arc-num {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
inline-size: 2.2rem;
|
||||
block-size: 2.2rem;
|
||||
border-radius: 50%;
|
||||
background: var(--rail);
|
||||
color: var(--oki-noir);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-1);
|
||||
}
|
||||
|
||||
.arc-advanced .arc-num {
|
||||
color: oklch(98% 0 0);
|
||||
}
|
||||
|
||||
.arc-diff {
|
||||
font-family: var(--font-text);
|
||||
font-size: var(--text-0);
|
||||
font-weight: 400;
|
||||
color: var(--oki-ink-2);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 0.1em 0.6em;
|
||||
}
|
||||
|
||||
.arc ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.arc a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
.arc a:hover {
|
||||
border-color: var(--rail);
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: var(--text-0);
|
||||
color: var(--oki-ink-2);
|
||||
background: var(--oki-surface-2);
|
||||
border-radius: var(--radius-2);
|
||||
padding: 0.05em 0.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
import { NEW_TOOL_ISSUE_URL, REPO_URL } from '$lib/site'
|
||||
|
||||
// de la plus simple à la plus complète (divulgation progressive, §1.3)
|
||||
const WAYS = [
|
||||
{ key: 'suggest', href: NEW_TOOL_ISSUE_URL },
|
||||
{ key: 'fix', href: `${REPO_URL}/src/branch/main/content/outils` },
|
||||
{ key: 'translate', href: `${REPO_URL}/src/branch/main/docs/CONTENT_GUIDE.md` }
|
||||
] as const
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{t('contrib.title')} — {t('site.name')}</title>
|
||||
<meta name="description" content={t('contrib.intro')} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="stack">
|
||||
<h1>{t('contrib.title')}</h1>
|
||||
<p class="prose">{t('contrib.intro')}</p>
|
||||
</header>
|
||||
|
||||
<ol class="ways stack" style="--gap: var(--space-4)">
|
||||
{#each WAYS as way, i (way.key)}
|
||||
<li class="way stack">
|
||||
<h2><span class="num" aria-hidden="true">{i + 1}</span>{t(`contrib.${way.key}`)}</h2>
|
||||
<p class="prose">{t(`contrib.${way.key}Body`)}</p>
|
||||
<p><a class="action" href={way.href} rel="external">{t(`contrib.${way.key}Cta`)} ↗</a></p>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
|
||||
<section class="repo stack">
|
||||
<h2>{t('contrib.repo')}</h2>
|
||||
<p class="prose">{t('contrib.repoBody')}</p>
|
||||
<p><a class="action" href={REPO_URL} rel="external">{t('contrib.repoCta')} ↗</a></p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.ways {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.way {
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.way h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
font-size: var(--text-3);
|
||||
}
|
||||
|
||||
.num {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
inline-size: 2.2rem;
|
||||
block-size: 2.2rem;
|
||||
border-radius: 50%;
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
font-size: var(--text-1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-4);
|
||||
border: 2px solid var(--oki-jaune);
|
||||
border-radius: var(--radius-2);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
background: var(--oki-jaune);
|
||||
color: var(--oki-noir);
|
||||
}
|
||||
|
||||
.repo {
|
||||
border-inline-start: 4px solid var(--oki-vert);
|
||||
padding-inline-start: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { loadContent } from '$lib/content/tools.server'
|
||||
|
||||
export function load() {
|
||||
const { tools, categories } = loadContent()
|
||||
return {
|
||||
categories,
|
||||
// seules les données de carte partent au client (budget JS, doctrine §4.4)
|
||||
tools: tools.map(({ sections: _sections, ...card }) => card)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment'
|
||||
import { page } from '$app/state'
|
||||
import CategoryNav from '$lib/components/CategoryNav.svelte'
|
||||
import SearchBar from '$lib/components/SearchBar.svelte'
|
||||
import ToolCard from '$lib/components/ToolCard.svelte'
|
||||
import EndMarker from '$lib/components/EndMarker.svelte'
|
||||
import DifficultyBadge from '$lib/components/DifficultyBadge.svelte'
|
||||
import { t } from '$lib/i18n/index.svelte'
|
||||
|
||||
let { data } = $props()
|
||||
let query = $state('')
|
||||
|
||||
const DIFFICULTIES = ['beginner', 'intermediate', 'advanced'] as const
|
||||
|
||||
// Chaque filtre vit dans l'URL (query param) : partageable (mission §5.1).
|
||||
// Le HTML prérendu montre la liste complète ; le filtrage est une
|
||||
// amélioration progressive côté client (ADR-001). Les searchParams ne
|
||||
// sont pas lisibles au prerender, d'où la garde `browser`.
|
||||
let cat = $derived(browser ? page.url.searchParams.get('cat') : null)
|
||||
let diff = $derived(browser ? page.url.searchParams.get('diff') : null)
|
||||
|
||||
let filtered = $derived(
|
||||
data.tools.filter(
|
||||
(tool) =>
|
||||
(!cat || tool.category === cat) &&
|
||||
(!diff || tool.difficulty === diff) &&
|
||||
(!query ||
|
||||
`${tool.name} ${tool.tags.join(' ')} ${tool.excerpt}`
|
||||
.toLocaleLowerCase('fr')
|
||||
.includes(query.toLocaleLowerCase('fr')))
|
||||
)
|
||||
)
|
||||
|
||||
function diffHref(d: string | null): string {
|
||||
const params = new URLSearchParams(browser ? page.url.searchParams : undefined)
|
||||
if (cat) params.set('cat', cat)
|
||||
if (d) params.set('diff', d)
|
||||
else params.delete('diff')
|
||||
const qs = params.toString()
|
||||
return qs ? `/outils?${qs}` : '/outils'
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{t('directory.title')} — {t('site.name')}</title>
|
||||
<meta name="description" content={t('directory.intro')} />
|
||||
</svelte:head>
|
||||
|
||||
<header class="stack">
|
||||
<h1>{t('directory.title')}</h1>
|
||||
<p>{t('directory.intro')}</p>
|
||||
</header>
|
||||
|
||||
<div class="stack filters">
|
||||
<SearchBar bind:value={query} />
|
||||
<CategoryNav categories={data.categories} />
|
||||
<nav aria-label={t('directory.filterDifficulty')}>
|
||||
<ul class="cluster">
|
||||
<li>
|
||||
<a href={diffHref(null)} aria-current={diff === null ? 'page' : undefined}>
|
||||
{t('directory.allDifficulties')}
|
||||
</a>
|
||||
</li>
|
||||
{#each DIFFICULTIES as d (d)}
|
||||
<li>
|
||||
<a href={diffHref(d)} aria-current={diff === d ? 'page' : undefined}>
|
||||
<DifficultyBadge difficulty={d} />
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<section class="stack" aria-live="polite">
|
||||
<!-- h2 : préserve l'ordre des titres (h1 → h2 → h3 des cartes) -->
|
||||
<h2 class="count">
|
||||
{filtered.length}
|
||||
{filtered.length === 1 ? t('directory.resultOne') : t('directory.results')}
|
||||
</h2>
|
||||
|
||||
{#if filtered.length === 0}
|
||||
<p class="empty">{t('directory.empty')}</p>
|
||||
{:else}
|
||||
<div class="card-grid">
|
||||
<ul>
|
||||
{#each filtered as tool (tool.slug)}
|
||||
<li><ToolCard {tool} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<EndMarker />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.filters {
|
||||
--gap: var(--space-3);
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: var(--tap-target);
|
||||
padding-inline: var(--space-3);
|
||||
border: 1px solid var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-decoration: none;
|
||||
color: var(--oki-ink);
|
||||
}
|
||||
|
||||
nav a[aria-current='page'] {
|
||||
background: var(--oki-surface-2);
|
||||
border-color: var(--oki-ink);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--oki-ink-2);
|
||||
font-size: var(--text-0);
|
||||
font-family: var(--font-text);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: var(--space-5);
|
||||
border: 1px dashed var(--oki-line);
|
||||
border-radius: var(--radius-2);
|
||||
text-align: center;
|
||||
color: var(--oki-ink-2);
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user