Projets

Au Cœur du Népal

Plateforme de réservation et d’exploitation pour une agence de trek francophone

Performance
93
Accessibility
96
Best Practices
100
SEO
100
Année
2022
Type
Projet client
Rôle
Ingénierie full-stack et infrastructure
Statut
En ligne
Page d’accueil d’Au Cœur du Népal : une crête himalayenne au crépuscule derrière le titre « Le Népal, n’en rêvez plus, allez-y. » et des boutons vers les treks.

Une plateforme en français pour une agence de trek basée à Katmandou : catalogue de treks et d’ascensions, réservations sur la capacité réelle des départs, espace d’équipe pour les guides et les affectations, et blog éditorial. Je l’ai construite de bout en bout, du site Next.js et de l’API Django à la pile Docker de production.

Contexte

Au Cœur du Népal est une agence de trek fondée à Katmandou par des Népalais francophones. Sa clientèle est francophone : tout le site public, le parcours de réservation et les outils de l’équipe fonctionnent en français.

L’agence avait besoin de plus qu’une vitrine : un catalogue de treks, d’ascensions et de programmes culturels avec des départs datés, des réservations qui respectent le nombre réel de places, et un espace privé où l’équipe, les guides et les porteurs voient leurs affectations.

Mon rôle

J’ai conçu le modèle de données et construit l’ensemble du système : le frontend Next.js, l’API REST Django, les interfaces d’administration et d’exploitation, et l’infrastructure de production (pile Compose, Nginx, sauvegardes, CI et déploiement).

Approche

Le backend est découpé en applications qui suivent le métier : catalogue (treks, ascensions, programmes), réservations, exploitation (équipe, disponibilités, affectations) et contenu (articles, témoignages, FAQ). Chaque application confine les écritures aux services et les lectures aux sélecteurs : les vues restent minces et chaque règle métier a une seule place.

Le site public est rendu statiquement et revalidé à la publication : quand un rédacteur enregistre, Django met en file une tâche Celery qui indique à Next.js les étiquettes à reconstruire. Les pages restent aussi rapides que des fichiers statiques, sans délai de cache pour les rédacteurs.

Architecture du système

Client

  • Navigateur · Voyageurs et équipe

    Les voyageurs consultent les treks et réservent ; l’équipe et les guides utilisent l’administration privée. Tout est pensé en français.

    Appelle Nginx

Périphérie

  • Nginx · TLS · deux domaines

    Termine le TLS pour le site et l’API, fixe les en-têtes de cache des images optimisées et limite la taille des envois par route avant que la requête n’atteigne l’application.

    Appelle Next.js, Django · DRF · Appelé par Navigateur

Application

  • Next.js · SSR · ISR · BFF

    Rend les pages publiques de façon statique et les revalide à chaque publication. Les jetons d’authentification restent dans des cookies HttpOnly derrière des routes de même origine.

    Appelle Django · DRF · Appelé par Nginx

  • Django · DRF · Gunicorn · API REST

    API du catalogue, des réservations, de l’exploitation et du contenu, avec séparation services/sélecteurs. La capacité d’un départ est revérifiée sous verrou.

    Appelle PostgreSQL 17, Redis 7, Cloudflare R2, LanguageTool · Appelé par Nginx, Next.js, GitHub Actions

  • Worker Celery · E-mails · revalidation

    Envoie les e-mails transactionnels et indique à Next.js quelles pages reconstruire, hors du chemin de la requête, avec reprises.

    Appelé par Redis 7

  • Beat · Planificateur

    Planification en base pour le nettoyage des sessions et jetons expirés.

    Appelle Redis 7 (asynchrone)

Données et services

  • PostgreSQL 17 · Stockage principal

    Catalogue, départs, réservations, affectations et contenu. L’application s’y connecte avec un rôle restreint.

    Appelé par Django · DRF, Sauvegardes

  • Redis 7 · Cache · file

    Cache, broker Celery, état de révocation et de verrouillage, dans des bases logiques séparées pour qu’un vidage du cache ne perde aucune tâche.

    Appelle Worker Celery (asynchrone) · Appelé par Django · DRF, Beat

  • Cloudflare R2 · media.aucoeurdunepal.com

    Stockage objet des photos et vidéos. Les envois sont décodés dans des limites de pixels, réorientés, ramenés à 2560 px et débarrassés de leurs métadonnées.

    Appelé par Django · DRF

  • LanguageTool · Instance privée

    Un correcteur grammatical français auto-hébergé pour les rédacteurs. Les textes sont relus sans quitter le serveur.

    Appelé par Django · DRF

Exploitation

  • Sauvegardes · Dump · restauration vérifiée

    Sauvegardes planifiées de la base, dont la restauration est éprouvée par la suite de validation de production plutôt que supposée.

    Appelle PostgreSQL 17 (asynchrone)

  • GitHub Actions · Tests · publication · déploiement

    Les vérifications backend, frontend et conteneurs conditionnent la publication des images sur GHCR ; le déploiement utilise exactement les images validées, immuables.

    Appelle Django · DRF (asynchrone)

Décisions techniques

Pourquoi Django REST Framework derrière un frontend Next.js ?

Décision

Une API Django distincte détient toutes les données et règles métier ; Next.js se charge du rendu et ne touche jamais la base.

Pourquoi

Les problèmes les plus durs de l’agence sont relationnels : départs, capacité, affectations, permissions. L’ORM de Django, ses migrations, ses transactions et son admin y répondent directement, et la même API sert le site public, les outils de l’équipe et l’administration.

Alternatives envisagées

Des route handlers Next.js avec un ORM comme Prisma, ou un CMS headless hébergé.

Compromis

Deux environnements d’exécution à construire, déployer et surveiller, et un contrat d’API à maintenir. En échange, la logique métier est testable en un seul endroit, indépendamment du rendu.

Pourquoi des jetons en cookies HttpOnly derrière un BFF ?

Décision

Les requêtes du navigateur passent par des route handlers Next.js de même origine, qui conservent les jetons dans des cookies HttpOnly et appellent Django côté serveur.

Pourquoi

Aucun script de la page ne peut lire un jeton, et la rotation des jetons avec détection de rejeu fonctionne sans que le navigateur manipule de secret.

Alternatives envisagées

Des jetons dans localStorage avec un en-tête Authorization.

Compromis

Un saut supplémentaire pour les appels authentifiés, et une vérification d’origine sur chaque mutation.

Interface

Liste des treks classiques avec un champ de recherche et des filtres par difficulté et par durée.
Le catalogue : la recherche et les filtres interrogent l’API paginée plutôt qu’une première page figée.
acdn hero

Difficultés d’ingénierie

  1. Une réservation pouvait tomber sur un départ tout juste fermé

    1. Problème

      La création d’une réservation vérifiait la capacité sous verrou, mais vérifiait avant de prendre ce verrou si le départ était encore ouvert (ni fermé, ni annulé, ni passé).

    2. Contrainte

      Les reprises devaient rester idempotentes : un client qui renvoie après un délai dépassé doit retrouver sa réservation, pas en créer une seconde.

    3. Solution

      La vérification d’éligibilité est passée dans la transaction, après la recherche de reprise idempotente et sous le même verrou d’inventaire que la vérification de capacité.

    4. Résultat

      Capacité et éligibilité sont désormais décidées de façon atomique, et une ancienne saisie malformée renvoie une erreur de validation au lieu d’une erreur de base.

  2. Des envois légers sur disque mais énormes en mémoire

    1. Problème

      Une image compressée peut se décoder en un bitmap énorme, et les photos originales portaient des métadonnées privées (dont le GPS) et des dimensions bien supérieures à tout affichage du site.

    2. Analyse

      Des tests avec des fichiers construits pour l’occasion ont montré que le décodeur allouait la mémoire avant toute vérification de taille.

    3. Solution

      Les pixels et les images d’animation sont bornés avant décodage ; les images sont orientées, ramenées à 2560 px au plus, réencodées en WebP et débarrassées de leurs métadonnées.

    4. Résultat

      Des fichiers plus légers, aucune position GPS issue des téléphones des guides, et des tests sur la suppression EXIF, la transparence, les animations et les fichiers invalides. Les médias existants demandent une migration séparée et relue.

  3. robots.txt cachait le site au moteur de rendu qu’il devait servir

    1. Problème

      Le robots.txt en ligne interdisait /_next/, qui contient les scripts et images optimisées nécessaires aux robots pour rendre une page Next.js ; par ailleurs, la collecte du plan de site s’arrêtait aux 100 premiers éléments.

    2. Solution

      Les ressources de rendu sont de nouveau accessibles, les routes privées sont exclues explicitement, et le plan de site comme le catalogue parcourent toutes les pages de l’API.

    3. Résultat

      Les moteurs de recherche voient les pages comme les visiteurs, et un grand catalogue ne disparaît plus du plan de site.

Performance, sécurité et accessibilité

Une revue qualité de production, en septembre 2026, a couvert la sécurité, la performance et l’accessibilité, sur le site en ligne comme sur les conteneurs de production. Elle a conduit à des mises à jour de dépendances fermant toutes les alertes ouvertes, à des limites de décodage des images envoyées, à un menu mobile qui retient le focus, à l’accès des robots aux ressources de rendu et à une pagination complète du plan de site. Les tests backend tournent sur PostgreSQL ; les tests navigateur, sur les vraies images de production en HTTPS.

Infrastructure et déploiement

La production est une pile Compose unique sur un VPS : Nginx, Next.js, Gunicorn, un worker Celery et Beat, PostgreSQL 17, Redis 7, une instance privée de LanguageTool et un service de sauvegarde. Les images sont construites une fois par commit, publiées sur GHCR seulement après la réussite des vérifications backend, frontend et conteneurs, puis déployées par référence immuable : ce qui tourne est exactement ce qui a été testé.

Enseignements

La plupart des défauts sérieux se trouvaient aux frontières (entre un verrou et une vérification, entre un envoi et un décodeur, entre robots.txt et le moteur de rendu), pas à l’intérieur d’un composant. C’est en testant les vraies images de production, et non un serveur de développement, qu’ils sont apparus.

Technologies

  • TypeScript
  • Next.js
  • Django
  • Django REST Framework
  • Celery
  • PostgreSQL
  • Redis
  • Cloudflare R2
  • Docker
  • Nginx
  • GitHub Actions
  • Playwright
  • LanguageTool
esc