Retour au projet

Shohraty

Comment le site web d'une agence marketing a fini par exiger un versionnement de schéma immuable, et ce que coûte vraiment un RTL pensé d'abord.

Vue d'ensemble

Shohraty est une agence de marketing digital à Alger. Pendant six mois, j'ai construit la plateforme sur laquelle elle tourne : un site public trilingue, un back-office d'administration que son personnel utilise tous les jours, et l'API et la base de données qui les portent. Trois dépôts, 513 commits, 58 000 lignes. Elle est passée en ligne sur le VPS du client en version 1.0.0 et affiche sa page d'accueil arabe en environ une seconde.

Problème

L'agence chiffrait des travaux sur mesure avec un site statique et recueillait ses demandes par téléphone. Chaque nouveau service exigeait un développeur ; chaque changement de ce qu'elle demandait aux prospects aussi. Elle avait besoin de maîtriser son contenu, son catalogue de services, ses formulaires de demande, ses offres d'emploi et sa boutique — d'abord en arabe, avec le français et l'anglais à côté — sans développeur.

Contexte

Deux contraintes ont tout façonné. Le public est algérien : l'arabe est donc la langue par défaut et le droite-à-gauche est le cas de base, pas une variante. Et le client voulait le système sur une infrastructure qu'il contrôle, ce qui écartait les réglages par défaut des plateformes gérées et faisait du déploiement, du TLS, du service des fichiers statiques et des sauvegardes une partie du travail plutôt que le problème de quelqu'un d'autre.

Mon rôle

Ingénieur principal. J'ai conçu le modèle de données, écrit l'API, construit le back-office, construit le site public, et déployé et durci le tout. Pendant le premier mois, j'ai collaboré avec l'équipe de l'agence pour que nous puissions terminer le projet ; à partir de juin, j'ai travaillé seul dessus. L'architecture, le schéma et la livraison étaient les miens.

Objectifs

  • Permettre au personnel non technique de modifier chaque élément du contenu public sans déploiement
  • Livrer l'arabe, le français et l'anglais à partir d'un seul arbre de contenu, avec l'arabe par défaut
  • Remplacer la prise de demandes par téléphone par des formulaires structurés par service que l'agence peut modifier
  • Garder tout le système sur une infrastructure que le client possède
  • Respecter la loi algérienne sur la protection des données plutôt que d'y plaquer une bannière de cookies générique

Décisions techniques

  • Deux surfaces d'API sur un seul schéma Prisma — /api/public/* ouverte et limitée, /api/admin/* derrière un cookie JWT plus CSRF — pour que les lectures publiques ne puissent jamais atteindre un chemin de modification
  • Les formulaires de demande de service sont des données, pas du code : un administrateur définit étapes et champs en trois langues et le backend enregistre le résultat comme ServiceFormVersion immuable
  • Le versionnement est déclenché par la forme des colonnes, pas par n'importe quelle modification. Renommer un libellé ou réordonner des champs met à jour en place ; changer la clé, le type ou les options d'un champ crée la v2. Sinon, chaque correction de formulation orphelinerait les soumissions existantes
  • Chaque soumission enregistre le formVersionId auquel elle a répondu, si bien qu'une ancienne demande reste lisible avec le schéma sur lequel elle a réellement été remplie
  • La synchronisation Google Sheets est asynchrone avec statut par ligne, nombre de tentatives et calendrier de reprise, car l'agence vit dans des tableurs et un export raté doit être visible et récupérable plutôt que silencieux
  • Un champ piège (honeypot) plus une limite de cinq envois par heure au lieu d'un captcha — le public est sur données mobiles et un captcha coûte plus de conversions que le spam
  • next-intl avec la langue comme segment de route et la direction fixée une fois à la racine de la mise en page, pour qu'aucun composant ne décide de sa propre direction

Architecture

Le site public Next.js lit depuis la surface d'API ouverte ; le back-office Refine écrit via la surface protégée. Les deux atteignent la même application NestJS — 21 modules, 147 endpoints — sur un seul schéma Prisma de 30 modèles. Le générateur de formulaires du back-office écrit des versions de schéma immuables ; l'assistant public affiche la version courante ; chaque soumission est estampillée de la version à laquelle elle a répondu puis recopiée, par une tâche de fond avec reprises, dans un onglet Google Sheets nommé d'après ce service et cette version.

Carte du système

3 dépôts / 1 schéma
Site publicNext 16 · AR/FR/EN · RTL16 k lignes · 234 commitsBack-officeRefine · TipTap · Vite18 k lignes · 145 commits/api/public/*ouvert · limité/api/admin/*JWT cookie + CSRFNestJS 1121 modules147 endpoints48 fichiers de testPostgreSQLPrisma 7 · 30 modelsServiceFormVersionschéma immuableétapes → champs · AR/FR/ENSheetsun onglet parservice-vNlitécritassistant en 8 étapesrepriseDÉPLOYÉshohraty.dz · VPS du client · nginx · v1.0.0MESURÉ LE 4 SEPT. 2026TTFB 87 ms · FCP 568 ms · LCP 1,02 s · 63 Ko
Deux applications clientes, deux surfaces d'API, un seul schéma. Le chemin violet est la partie à interroger : un administrateur compose un formulaire, le backend le fige en version immuable, l'assistant public affiche cette version, et chaque soumission est estampillée de la version à laquelle elle a répondu avant qu'une tâche avec reprises ne la recopie dans un onglet de tableur propre à cette version.

Captures d'écran de l'étude de cas

7 vues
La page d'accueil arabe en ligne. L'arabe est la langue par défaut et toute la mise en page est de droite à gauche, pas un miroir ajouté après coup.
La même page en anglais. La direction, l'ordre de la navigation et la suite des cartes s'inversent à partir d'un seul arbre de contenu — la paire est la preuve la plus claire que le travail RTL est réel.
Étape 3 sur 8 de l'assistant de demande de service. Aucun de ces champs n'est dans le code du frontend : un administrateur les a composés en trois langues et le backend a figé cette définition en version de formulaire immuable.
L'assistant affiche ce que la version courante du schéma définit, y compris le nombre d'étapes. L'indicateur de progression est généré, pas codé en dur.
Les services sont des enregistrements du CMS, pas des routes. Chacun porte ses propres traductions, sa FAQ, ses phases de travail et son formulaire de demande.
Le consentement repose sur la loi algérienne 18-07, avec adhésion par catégorie. Les pixels d'analyse et publicitaires ne se déclenchent via GTM qu'une fois la catégorie correspondante acceptée.
La page d'accueil arabe à 390 px. Le RTL doit tenir à chaque point de rupture, pas seulement sur ordinateur.

Fonctionnalités clés

  • Demandes de service multi-étapes composées par les administrateurs, trilingues, affichées à partir d'un schéma figé
  • Synchronisation Google Sheets par ligne avec reprises, nombre de tentatives et resynchronisation manuelle
  • Couverture CMS complète : sections d'accueil, services, blog, offres d'emploi, FAQ, contact, boutique, commandes et paramètres
  • Consentement aux cookies conforme à la loi algérienne 18-07 avec adhésion par catégorie via GTM
  • Spécificités du commerce algérien : DZD, données de wilayas et de communes, validation des numéros locaux, CCP et BaridiMob

Défis

  • Du contenu arabe contenant des noms de marque latins s'affichait avec la ponctuation du mauvais côté. La correction a consisté à cesser de forcer dir=rtl sur les nœuds de contenu dynamique et à utiliser dir=auto sur les conteneurs de prose, pour laisser le navigateur résoudre la direction chaîne par chaîne
  • La première version de la page de détail d'un service répartissait les blocs de description par position dans trois emplacements fixes, si bien qu'un administrateur qui ajoutait un quatrième bloc le perdait silencieusement. Remplacé par une liste ordonnée de blocs affichée en entier
  • Les téléversements de couverture à la création se couraient après l'enregistrement parent : l'envoi partait avant que le service ou l'article existe, orphelinant une ligne de média, puis repartait avec l'identifiant du parent. Différer l'envoi après la création du parent a supprimé le doublon comme l'orphelin
  • Le repli de langue renvoyait un tableau vide au lieu de n'importe quelle traduction disponible quand la langue demandée manquait, si bien qu'un enregistrement partiellement traduit semblait cassé au lieu d'être dégradé
  • Déployer sur un VPS appartenant au client voulait dire que l'optimiseur d'images, les fichiers statiques téléversés et le TLS devaient être configurés plutôt que supposés, et un mauvais motif distant se traduit en erreurs 400 sur chaque image au lieu d'un repli silencieux

Résultats

  • En ligne sur shohraty.dz, sur le VPS du client, en version 1.0.0, dernière livraison en septembre 2026
  • Mesuré depuis le site public : 87 ms jusqu'au premier octet, 568 ms de First Contentful Paint, 1,02 s de Largest Contentful Paint, 63 Ko transférés en 57 requêtes
  • 147 endpoints sur 21 modules, 30 modèles Prisma, 48 fichiers de test
  • Environ 650 chaînes traduites par langue en arabe, français et anglais
  • Chaque page publique — contenu, services, formulaires, offres d'emploi, FAQ, boutique — est désormais modifiable par le personnel de l'agence sans déploiement

Ce que j'ai appris

  • Traiter un formulaire comme des données plutôt que du code déplace le problème difficile du frontend vers l'évolution du schéma, ce qui est le bon endroit — mais seulement si le versionnement est déclenché par la forme plutôt que par n'importe quelle modification, sinon les versions se multiplient jusqu'à ne plus rien vouloir dire
  • Le RTL n'est pas une feuille de style. C'est une valeur par défaut qui doit tenir à chaque point de rupture, pour chaque chaîne dynamique et chaque composant qui croit savoir dans quel sens va le texte
  • Une synchronisation en arrière-plan n'est digne de confiance que si ses échecs sont visibles. Un statut par ligne et un nombre de tentatives ont transformé une intégration invisible en une intégration que le client peut exploiter
  • Posséder l'infrastructure déplace le travail plutôt que de le supprimer. Les performances sont meilleures que celles qu'aurait données un déploiement géré à ce prix, et le coût est que chaque maillon du chemin de service est désormais à moi de configurer

Améliorations futures

  • Sortir les téléversements du disque local pour que le pipeline de médias survive à la reconstruction de la machine
  • Repli de langue côté backend : renvoyer n'importe quelle traduction disponible quand la langue demandée manque, plutôt qu'un résultat vide
  • Corriger la racine https nue, qui se bloque par intermittence avant de se résoudre alors que les routes par langue répondent immédiatement
  • Des contrôles visuels automatisés sur les arbres arabe et anglais, car les régressions de direction sont faciles à livrer et difficiles à remarquer