Documentation
Installation
Prérequis : Docker et Docker Compose, ainsi qu'un fournisseur OIDC (PocketID, Authentik, Keycloak, etc.).
Option A (image pré-construite)
curl -O https://raw.githubusercontent.com/notarobot63/excerpta/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/notarobot63/excerpta/main/.env.example
cp .env.example .env
# éditer .env
REGISTRY_IMAGE=ghcr.io/notarobot63/excerpta:latest \
docker compose -f docker-compose.prod.yml up -d
Option B (build local)
git clone https://github.com/notarobot63/excerpta.git
cd excerpta
cp .env.example .env
# éditer .env
docker compose up --build -d
Variables d'environnement
| Variable | Obligatoire | Description |
|---|---|---|
SECRET_KEY | Oui | Clé secrète des sessions, générée avec python3 -c "import secrets; print(secrets.token_hex(32))" |
BASE_URL | Oui | URL publique complète (ex. https://liens.exemple.com) |
OIDC_CLIENT_ID | Oui | Client ID de l'application OIDC |
OIDC_CLIENT_SECRET | Oui | Client secret de l'application OIDC |
OIDC_ISSUER | Oui | URL de l'issuer OIDC (ex. https://auth.exemple.com) |
FRESHRSS_SYNC_INTERVAL | Non | Intervalle de synchronisation FreshRSS en minutes (défaut : 30) |
Reverse proxy
Excerpta écoute sur le port 8070 (configurable dans le compose). Exemple Caddy :
liens.exemple.com {
reverse_proxy localhost:8070
}
Exemple Nginx :
server {
listen 443 ssl;
server_name liens.exemple.com;
location / {
proxy_pass http://localhost:8070;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Le header
X-Forwarded-Forest nécessaire pour que la limitation de débit fonctionne correctement par IP client.
Mise à jour
# image pré-construite
REGISTRY_IMAGE=ghcr.io/notarobot63/excerpta:latest \
docker compose -f docker-compose.prod.yml pull && \
docker compose -f docker-compose.prod.yml up -d
# build local
git pull && docker compose up --build -d
Données persistantes
Les données (base SQLite) sont stockées dans un volume Docker nommé excerpta_data. Pour sauvegarder :
docker run --rm \
-v excerpta_data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/excerpta-backup.tar.gz /data
Configuration
OIDC / Authentification
Excerpta utilise OIDC avec le flux PKCE. N'importe quel fournisseur compatible est supporté.
PocketID
- Créer une application OIDC dans PocketID
- URL de callback :
https://votre-instance.exemple.com/auth/oidc/callback - Renseigner
OIDC_CLIENT_ID,OIDC_CLIENT_SECRETetOIDC_ISSUERdans.env
Authentik
- Créer un Provider OAuth2/OIDC (type : Authorization Code + PKCE)
- URL de callback :
https://votre-instance.exemple.com/auth/oidc/callback OIDC_ISSUER=https://auth.exemple.com/application/o/<slug>/
Keycloak
- Créer un client Keycloak, Access Type
public, PKCE activé OIDC_ISSUER=https://keycloak.exemple.com/realms/<realm>
Thèmes
9 palettes disponibles, chacune en variante claire et sombre. Le réglage se fait depuis la barre latérale et reste mémorisé dans le navigateur.
| Palette | Clair | Sombre |
|---|---|---|
| Par défaut | Light | Dark |
| Nord | Nord | Nord Dark |
| Dracula | Dracula Light | Dracula |
| Catppuccin | Latte | Mocha |
| Gruvbox | Gruvbox Light | Gruvbox |
| Solarized | Solarized | Solarized Dark |
| Rosé Pine | Dawn | Moon |
Le sélecteur complet est dans Paramètres → Apparence.
Clé API
La clé de l'API REST est générée automatiquement à la création du compte. Visible et régénérable dans Paramètres → Compte. Elle est stockée sous forme de HMAC, la valeur en clair n'est jamais conservée côté serveur.
Bookmarklet
Disponible dans Paramètres → Bookmarklet. Le glisser dans la barre de favoris du navigateur permet d'enregistrer la page courante en un clic.
Import / export
- Import : liens au format Netscape HTML (Firefox, Chrome, Safari), les dossiers sont recréés automatiquement
- Export :
Paramètres → Export: génère un fichier Netscape HTML avec dossiers et tags
Raccourcis clavier
| Touche | Action |
|---|---|
n | Nouveau lien |
/ | Focus sur la recherche |
Les raccourcis sont désactivés quand le focus est dans un champ de saisie.
Détection de doublons
Si vous tentez d'ajouter une URL déjà présente dans votre collection (via le formulaire ou le bookmarklet), Excerpta redirige automatiquement vers la page d'édition du lien existant avec un avertissement.
Administration
Le panneau d'administration (/admin/) n'est accessible qu'aux comptes marqués is_admin = true en base. Il permet de lister et désactiver des utilisateurs, consulter des statistiques globales, et régénérer des clés API.
Recherche
La recherche est en temps réel : les résultats se filtrent pendant la frappe (sans rechargement), avec un léger délai de temporisation et un seuil de déclenchement à 2 caractères. Vider le champ réaffiche tous les liens. La pagination se fait aussi en AJAX et l'URL reste partageable (le bouton retour du navigateur fonctionne).
L'index plein texte (SQLite FTS5) couvre titres, descriptions, notes, URLs et tags, et est insensible aux accents. Sans JavaScript, le formulaire de recherche classique fonctionne toujours.
Les résultats sont classés par pertinence (bm25 pondéré) : un terme trouvé dans le titre pèse plus que dans les tags, la description ou l'URL. Les termes recherchés sont surlignés dans les titres affichés.
Vue lecteur
Chaque lien dispose d'une icône lecteur qui ouvre une version épurée et lisible de l'article :
- Extraction du contenu principal via Readability (le même algorithme que le mode lecture de Firefox), suivie d'une sanitisation HTML (anti-XSS)
- Présentation focalisée : colonne de lecture étroite, typographie soignée, images conservées, temps de lecture estimé
- Taille de police ajustable (mémorisée) et thème clair/sombre hérité de l'application
- L'extraction se fait à la première ouverture puis est mise en cache ; les ouvertures suivantes sont instantanées. Ajouter
?refresh=1à l'URL force une nouvelle extraction - Les pages non extractibles (paywall, contenu purement JavaScript) affichent un état d'échec avec un lien vers l'original
Archivage (Wayback Machine)
Excerpta archive les liens sur la Wayback Machine d'Internet Archive.
- Automatique à l'ajout : chaque nouveau lien est archivé en tâche de fond (capturé pendant que la page est encore vivante). La synchronisation FreshRSS n'archive pas automatiquement, pour éviter de saturer les quotas de Wayback
- Statut visible par lien sur sa carte : en cours, archivé (l'icône pointe vers la capture Wayback), ou échoué (bouton pour réessayer)
- Archivage en masse :
Paramètres → Archiver les liens non archivéslance une tâche de fond throttlée sur tous les liens pas encore archivés
Wayback limite fortement l'archivage anonyme : sur un gros lot, certains liens peuvent échouer (HTTP 429). Relancer ne cible que les liens encore non archivés.
Page publique
Chaque utilisateur dispose d'une page publique listant ses liens publics :
- URL :
/u/{slug}, leslugest personnalisable dans Paramètres → Page publique (voir Paramètres) - Flux RSS associé :
/u/{slug}/feed.xml - Seuls les liens marqués publics y apparaissent ; le titre de la page est personnalisable
API REST v1
Authentification
Toutes les requêtes à l'API nécessitent une clé API dans l'en-tête :
X-API-Key: <votre-clé-api>
La clé est disponible dans Paramètres → Compte. Limite de débit : 60 requêtes / minute par clé.
GET /api/v1/me
Retourne le profil de l'utilisateur authentifié.
curl https://votre-instance.exemple.com/api/v1/me \
-H "X-API-Key: <clé>"
GET /api/v1/links
Liste les liens, paginés, avec recherche et filtres optionnels.
| Paramètre | Type | Description |
|---|---|---|
q | string | Recherche plein texte (titre, description, note, URL, tags), insensible aux accents |
tag | string | Filtrer par tag |
group_id | int | Filtrer par dossier (inclut les sous-dossiers) |
page | int | Page (défaut : 1) |
per_page | int | Résultats par page (défaut : 30, max : 100) |
curl "https://votre-instance.exemple.com/api/v1/links?q=python&tag=dev&page=1" \
-H "X-API-Key: <clé>"
POST /api/v1/links
Crée un nouveau lien.
| Champ | Type | Description |
|---|---|---|
url | string | URL du lien (obligatoire) |
title | string | Titre (défaut : URL) |
note | string | Note markdown |
tags | array | Liste de tags |
folder_id | int | ID de dossier (optionnel, ignoré si invalide) |
is_public | bool | Rendre le lien public immédiatement (défaut : false) |
curl -X POST https://votre-instance.exemple.com/api/v1/links \
-H "X-API-Key: <clé>" \
-H "Content-Type: application/json" \
-d '{"url": "https://exemple.com", "title": "Exemple", "tags": ["dev", "tool"]}'
PATCH /api/v1/links/{id}
Met à jour un lien existant (par exemple is_public).
curl -X PATCH https://votre-instance.exemple.com/api/v1/links/42 \
-H "X-API-Key: <clé>" \
-H "Content-Type: application/json" \
-d '{"is_public": true}'
DELETE /api/v1/links/{id}
Supprime un lien. Retourne 204 No Content.
GET /api/v1/tags · /api/v1/folders
Liste les tags (avec leur nombre de liens associés) et l'arborescence des dossiers (avec leur nombre de liens).
POST /api/v1/freshrss/sync
Déclenche une synchronisation FreshRSS manuelle. Utile pour un cron externe.
GET /public/feed.xml
Flux RSS public, sans authentification. Retourne les 100 liens publics les plus récents au format RSS 2.0.
Codes d'erreur
| Code | Signification |
|---|---|
| 401 | Clé API manquante ou invalide |
| 404 | Ressource non trouvée |
| 422 | Données invalides |
| 429 | Limite de débit atteinte |
Intégration FreshRSS
Excerpta peut synchroniser automatiquement les articles étoilés de FreshRSS via l'API GReader.
Configuration
- Dans FreshRSS, activer l'API GReader : Paramètres → Authentification → Accès API
- Dans Excerpta : Paramètres → FreshRSS
| Champ | Description |
|---|---|
| URL FreshRSS | URL de l'instance (ex. https://rss.exemple.com) |
| Identifiant | Nom d'utilisateur FreshRSS |
| Mot de passe API | Mot de passe API GReader (peut différer du mot de passe principal) |
| Dossier cible | Dossier Excerpta où sont classés les articles importés |
| Intervalle | Fréquence de synchronisation en minutes (défaut : 30) |
Le jeton GReader est chiffré (Fernet) avant stockage.
Fonctionnement
À chaque synchronisation, Excerpta récupère tous les articles étoilés et importe ceux pas encore présents (dédoublonnés par URL). Les articles importés sont classés dans le dossier cible configuré : c'est ce dossier, pas un tag, qui les identifie comme venant de FreshRSS.
Désétoilage automatique
Supprimer un lien importé depuis FreshRSS propose de désétoiler l'article dans FreshRSS en même temps, via une case à cocher (cochée par défaut). Déplacer un lien hors de son dossier FreshRSS (glisser-déposer ou édition) le désétoile aussi automatiquement.
Application Android
L'application excerpta-android permet d'enregistrer des liens directement depuis le menu de partage d'Android.
Fonctionnalités
- Partager une URL depuis n'importe quelle application Android
- Ajouter un titre, une note et des tags avant l'envoi
- Authentification par clé API (stockée localement)
Installation
Disponible sur notarobot63/excerpta-android, avec des APK signés pré-construits en releases. Pas encore sur le Play Store.
Configuration
- Dans Excerpta, aller dans Paramètres → Compte
- Scanner le QR code avec l'application Android
Le QR code encode l'URL de l'instance et la clé API. Il est aussi directement disponible via GET /settings/android-qr.png (avec la clé API en en-tête).
Configuration manuelle
Si le scan n'est pas possible, saisir manuellement dans l'application :
- URL de l'instance :
https://votre-instance.exemple.com - Clé API : disponible dans Paramètres → Compte
Contribuer
Stack
| Couche | Technologie |
|---|---|
| Backend | Python 3.11+, FastAPI, SQLModel |
| Base de données | SQLite (WAL + FTS5) |
| Templates | Jinja2 |
| JS | Alpine.js (v3, servi localement) |
| CSS | CSS natif, variables de thème |
| Extraction lecteur | readability-lxml + nh3 (sanitisation HTML) |
| Tests | pytest (tests/, voir requirements-dev.txt) |
Environnement de développement
git clone https://github.com/notarobot63/excerpta.git
cd excerpta
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
# éditer .env
uvicorn app.main:app --reload
L'application est disponible sur http://localhost:8000.
Tests
pip install -r requirements-dev.txt
pytest -q
La CI GitLab exécute pytest à l'étape test, qui bloque le build et le déploiement en cas d'échec.
Sécurité
Avant de soumettre une contribution :
- Aucun secret en clair dans le code ou les templates
- Les URLs externes passent par
_safe_url()(liste noire SSRF) - Les formulaires POST incluent le token CSRF (
{{ csrf_input(request) }}) - Les requêtes JSON utilisent l'en-tête
X-CSRF-Token - Toute nouvelle route authentifiée utilise
Depends(get_current_user)
Signaler un problème
Ouvrir une issue sur le dépôt avec une description précise du comportement observé et les étapes pour le reproduire.