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émaCaptures d'écran de l'étude de cas
7 vuesFonctionnalité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