CNAM+ Simple est une application web scraper dédiée à l'extraction et la mise en cache des données de la formation continue du CNAM (Conservatoire National des Arts et Métiers) depuis le portail bedeo.cnam.fr.
Le projet est construit selon une architecture monorepo Turbo basée sur le template SCTG Vite/React avec:
- Frontend: Application React/Vite (client léger)
- Backend: Cloudflare Worker (API serverless)
- Scraper les données de cursus du site CNAM de maniÚre efficace et fiable
- Mettre en cache les résultats pour réduire les appels aux serveurs CNAM
- Exposer une API REST sécurisée avec contrÎle de cache
- Supporter deux niveaux d'enrichissement:
- Level 1: Structure de Cursus (années, unités, codes)
- Level 2: Détails complets des unités (objectifs, contenu, bibliographie)
Le projet fonctionne complĂštement en local et fonctionne Ă©galement sur un Cloudflare Worker payant (ce dernier permet des durĂ©es CPU par requĂȘte plus longues nĂ©cessaires pour produire des descriptions complĂštes). En revanche, la version actuellement dĂ©ployĂ©e en production utilise un Worker sur le plan gratuit, dont les limites CPU par requĂȘte peuvent empĂȘcher la gĂ©nĂ©ration intĂ©grale des descriptions longues â d'oĂč des rĂ©ponses tronquĂ©es ou incomplĂštes en production.
Le fichier examples/CYC9101A.md a été généré sur un Worker du plan payant (c'est pourquoi il contient la description intégrale). Si vous voulez reproduire ce résultat en production, il est recommandé de déployer le service sur un Worker payant ou d'implémenter un job asynchrone cÎté backend pour traiter les enrichissements longue durée.
cnam+simple/
âââ apps/
â âââ client/ # Frontend React/Vite
â â âââ src/
â â â âââ components/ # Composants UI
â â â âââ pages/ # Pages de l'application
â â â âââ locales/ # Traductions (i18n)
â â âââ vite.config.ts
â âââ cloudflare-worker/ # Backend Cloudflare Worker
â âââ src/
â â âââ scraper/ # Logique de scraping
â â âââ cache/ # Gestion du cache KV
â â âââ routes/ # Handlers API
â â âââ utils/ # Utilitaires (validation, etc)
â â âââ __tests__/ # Tests unitaires
â âââ vitest.config.ts
â âââ wrangler.jsonc # Configuration Cloudflare
âââ turbo.json # Config monorepo
âââ package.json
âââ .env # Variables d'environnement
- Framework: React 19 avec TypeScript
- Build: Vite
- Styling: TailwindCSS + HeroUI
- i18n: Support multilingue (FR, EN, ES, AR, HE, ZH)
- Package Manager: Yarn
- Runtime: Cloudflare Workers (serverless)
- Browser: Cloudflare Playwright (headless automation)
- Storage: Cloudflare KV (clé-valeur distribuée)
- Language: TypeScript 5.9+
- Testing: Vitest 3.2+ avec Cloudflare vitest-pool-workers
- Timeout: Limite 30 secondes (contrainte Cloudflare)
@cloudflare/playwright: Automatisation via navigateur headlessjose: Gestion JWTsha512crypt-node: Validation de mots de passe SHA512-crypt
- Navigation vers
https://bedeo.cnam.fr/public/cursus/view/{code} - Extraction XPath du schéma de Cursus (
#cursus_schema) - Récupération:
- Années: Structure d'études
- Unités: Codes, noms, crédits par année
- Métadonnées: Objectifs, audience accessible
- Mise en cache 24h par défaut
- Pour chaque unité, navigation vers
https://bedeo.cnam.fr/public/unite/view/{code} - Extraction détaillée:
- Présentation: AccÚs audience, objectifs spécifiques
- Contenu: Description du contenu pédagogique
- Bibliographie: Références et ressources
- Concurrence contrĂŽlĂ©e: 2 requĂȘtes simultanĂ©es max + dĂ©lais entre batches
- Dégradation gracieuse: Retourne Level 1 si Level 2 échoue
GET /api/cursus/<code>
Exemples de requĂȘtes:
# Cursus basic (Level 1 avec cache)
curl "http://localhost:8787/api/cursus/CYC9101A"
# Enrichissement Level 2 (peut ĂȘtre long)
curl "http://localhost:8787/api/cursus/CYC9101A?enrich=true"
# Forcer un refresh (invalidation de cache) â REQUIERT une api-key valide
# Sans api-key la requĂȘte `?force=true` est ignorĂ©e et le cache est prĂ©servĂ©.
curl "http://localhost:8787/api/cursus/CYC9101A?api-key=cleartext&force=true"| ParamĂštre | Type | Description |
|---|---|---|
force |
boolean | Demander un scraping frais et invalider le cache uniquement si une api-key valide est fournie (sinon le paramÚtre est ignoré). |
timeout |
number | Timeout personnalisé en millisecondes (défaut: 30000) |
enrich |
boolean | Récupérer Level 2 (détails complets des unités) |
api-key |
string | Mot de passe en clair pour bypass cache + invalidation |
{
"success": true,
"data": {
"name": "Titre du Cursus",
"code": "CYC9101A",
"audience_access": "Tout public",
"objectives": "Description des objectifs",
"EU": [
{
"year": "1",
"units": [
{
"code": "UTC501",
"name": "Unité 1",
"credits": 6,
"audience_access": "...",
"objectives": "...",
"content": "...",
"bibliography": [
{ "title": "Livre", "author": "Auteur" }
]
}
]
}
]
},
"cached": false,
"scrapedAt": "2026-02-10T18:20:00Z"
}DELETE /api/cursus/<code>/cache
- Format: Hashes SHA512-crypt (OpenSSL
openss passwd -6) - Stockage: Variable d'env
SCRAPER_CACHE_OVERRIDE - Exemple de hash:
$6$CY9bJUwYHGVpqnJx$Yz...
Génération de hash:
openssl passwd -6
# Entrer le mot de passe et copier le hash résultant- TTL par défaut: 30 jours (configurable) via la variable d'environnement
SCRAPER_CACHE_TTL - Invalidation: Via password-protected endpoint (recommandé : utiliser
api-key+force=truepour invalider) - Override forcé: Requiert désormais une
api-keyvalide. Les requĂȘtes avec?force=truesansapi-keyne provoqueront pas d'invalidation du cache (sĂ©curitĂ© renforcĂ©e).
Remarque: lorsqu'un enrichissement Level 2 (?enrich=true) rĂ©ussit, le rĂ©sultat enrichi est sauvegardĂ© en cache â les requĂȘtes suivantes peuvent donc retourner des donnĂ©es enrichies mĂȘme sans ?enrich=true.
- Node.js 24+
- Yarn 4+
- Compte Cloudflare (pour production)
# Installer les dépendances
yarn install
# Générer les types Cloudflare
cd apps/cloudflare-worker
yarn cf-typegen:env# Démarrer l'environnement complet (client + worker)
yarn dev:env
# Frontend: http://localhost:5173
# Worker: http://localhost:8787cd apps/cloudflare-worker
yarn test --run # Test une fois
yarn test # Mode watch- Extraction XPath de Cursus
- Cache KV avec TTL
- Routes API GET/DELETE
- 46 tests unitaires validés
- Parsing détails unités (préentation, contenu, bibliographie)
- Concurrence contrÎlée (2 max)
- Dégradation gracieuse
- 53 tests totaux validés
- Import sha512crypt-node
- Validation password SHA512-crypt
- Query params
?enrich=trueet?api-key=xxx - Invalidation de cache sécurisée
- Tests d'intégration avec bedeo.cnam.fr réel
- UI React pour recherche de Cursus
- Optimisation pour limite 30s
- Déploiement Cloudflare
- Configuration CORS
- Rate limiting
- Monitoring et logs
- Documentation API complĂšte
Le projet utilise Vitest avec le pool @cloudflare/vitest-pool-workers pour tester dans l'environnement Cloudflare.
# Tests existants (53 total):
â cnam-scraper.test.ts (20 tests - unitaires)
â cursus-routes.test.ts (26 tests - contrats API)
â unit-parser.test.ts (7 tests - Level 2 parsing)- Tests rĂ©els: Valider sur bedeo.cnam.fr avec vrais codes de Cursuss
- Optimisation timeout: Gérer Level 2 complet malgré limite 30s
- Frontend: Composants React pour recherche et affichage
- Déploiement: Publication sur Cloudflare Workers
- Monitoring: Logs et métriques
# Cloudflare
CLOUDFLARE_BACKEND="http://localhost:8787"
# CNAM
CNAM_FORMATION_CODE="CYC9101A" # Code de formation pour les tests (ex: CYC9101A, CYC9202B, etc)
CNAM_BEDEO_URL="https://bedeo.cnam.fr"
CNAM_BEDEO_CURSUS_PATH="public/cursus/view/"
CNAM_BEDEO_UNITE_VIEW_PATH="/public/unite/view/"
# Performance
SCRAPER_CACHE_TTL=2592000 # 30 jours en secondes
# Sécurité
SCRAPER_CACHE_OVERRIDE="$6$..." # Hash SHA512-crypt
CORS_ORIGIN="http://localhost:5173"MIT License - Copyright (c) 2024-2026 Ronan LE MEILLAT
Les contributions sont bienvenues ! Veuillez:
- Créer une branche feature
- Ăcrire des tests
- Valider avec
yarn test - Soumettre une PR
DerniÚre mise à jour: 10 février 2026