DKBois - Menuiserie & Ebenisterie sur ordinateur portable
Application Web
En vedette

DKBois - Menuiserie & Ebenisterie

Application web pour une entreprise de menuiserie au Cameroun. Catalogue de services, demande de devis en ligne, espace admin.

Technologies

API REST
Django
PostgreSQL
Python
React
Tailwind CSS

A propos du projet

fdkbois.com est le site vitrine de DKbois, la menuiserie et ébénisterie de mon père à Yaoundé. Front HTML statique sur Vercel, backend Django sur Railway. En juillet 2026, j'y ai greffé un blog rendu côté serveur et réparé les pages projet que Google ne voyait pas, en corrigeant au passage une faille XSS trouvée en revue.

Le contexte : qui, pourquoi, quelle contrainte

DKbois fait du meuble sur mesure, du dressing et de la cuisine en bois massif à Yaoundé. C'est l'entreprise de mon père, et c'est moi qui gère le site. Je ne le compte pas comme une vente : c'est un actif de preuve, pas un mandat que je facture. Lui ajoute ses témoignages et ses photos par une page d'admin, donc le site vit sans moi au quotidien.

La stack existante : front HTML statique sur Vercel, bilingue FR/EN par un paramètre ?lang=, animations GSAP, backend Django/DRF sur Railway, médias sur Cloudinary. Le domaine canonique est l'apex fdkbois.com, le www redirige dessus.

La contrainte tient en une phrase : le site est en production et déjà indexé. Je n'avais pas le droit de le casser pour me faire plaisir côté architecture.

Le problème : ce qui coinçait vraiment

Dans le HTML brut que reçoit un robot, les pages projet contenaient un titre générique, « Projet - DKBois », et rien d'autre. Le contenu arrivait après, par JavaScript, côté client. Google voyait donc une coquille. Et de vieilles adresses en ?id=pN avaient été indexées puis étaient mortes.

À côté de ça, la vitrine était figée. Aucun contenu frais, donc aucune raison pour un robot de repasser.

Le pire est sorti d'une revue adversariale du code avant mise en ligne : une XSS stored non authentifiée. Le bloc JSON-LD des pages projet était sérialisé avec json.dumps, qui n'échappe pas le HTML. La séquence </script> dans une chaîne JSON ressort telle quelle, le navigateur referme le <script type="application/ld+json"> à cet endroit précis, et tout ce qui suit est interprété comme du JavaScript. Le viewset d'écriture était en AllowAny : n'importe qui pouvait POSTer un titre de projet piégé et faire exécuter son code sur une page publique du site de mon père.

Ce que j'ai construit : les choix techniques et pourquoi ceux-là

J'ai fait rendre le blog et les pages projet par Django, servis sous fdkbois.com par un rewrite Vercel (/blog, /blog/*, /project.html, /sitemap.xml pointent vers Railway).

Pas de Next.js, et c'était la vraie décision. Migrer, c'était réécrire 7 pages, l'admin SPA, les animations GSAP et l'i18n, sur un site live et indexé, pour un gain que Django me donnait déjà. Django rend des templates : il couvre le SSR sans que je touche au front existant. Next ne vaudrait le coup que le jour d'une refonte visuelle complète, où le surcoût tomberait à presque rien.

Le client qui va chercher les articles est volontairement pauvre : urllib de la stdlib, cache 300 s pour imiter l'ISR, erreurs mises en cache 15 s pour ne pas marteler l'API en panne, timeout 4 s. Il gère aussi les 301 redirect_to, les 404 et les traductions, ce qui alimente le hreflang.

Le piège qui m'a coûté du temps : Vercel sert le filesystem avant d'appliquer les rewrites. Tant que project.html et project.js existaient en statique, Django ne voyait jamais passer la requête. Il a fallu les supprimer.

Côté redirections : les vieilles ?id=pN renvoient un 301 vers /portfolio.html, un slug inconnu renvoie un vrai 404.

Le correctif XSS échappe <, >, & et U+2028/2029 avant le |safe. Le HTML des articles est nettoyé de ses <script>, <iframe>, attributs on* et URIs javascript: avant d'être rendu.

Le résultat : ce qui est mesurable et vrai

Livré et vérifié en production le 2026-07-15, commit 5911457. À la mise en ligne, le blog affichait son état vide : aucun article n'était encore publié.

Le site rankait déjà avant le blog. Au relevé du 2026-07-19, sans un seul article publié, il tenait des positions 1 à 3 sur des mots-clés locaux de Yaoundé. Le SSR n'a pas créé ces positions.

Au relevé du 19 août 2026, le site compte 12 mots-clés classés sur 23 suivis, un de plus qu'au relevé précédent, dont cinq en position 1. Trois mouvements de hausse ce jour-là : « ébéniste Douala » passe de 7 à 2, première percée réelle du cluster Douala, « prix dressing sur mesure Yaoundé » entre en 3, « devis menuiserie Cameroun » gagne une place et passe à 2. Ça bouge dans les deux sens : deux jours plus tôt, le relevé du 17 août enregistrait trois reculs, dont un mot-clé sorti du top.

Le AllowAny sur l'écriture est toujours la cause racine, l'échappement reste un pansement. La vérification du code servi aujourd'hui a échoué faute d'accès au dépôt depuis une autre machine, parce que fdkbois n'était pas dans ma carte des repos. Un avis de sécurité du 2026-07-16 a aussi relevé des identifiants admin faibles passés en clair, avec rotation à faire. Et la génération d'articles n'est pas autonome : au run du 2026-08-17, j'ai corrigé à la main une erreur d'essence de bois inventée par le générateur.

Ce que j'en retiens

Greffer bat refondre. Un site vivant qui ranke déjà ne se réécrit pas pour ajouter une section, on lui sert le neuf à côté par un proxy.

json.dumps n'est pas un échappeur HTML, et un échappement n'est pas une permission. J'ai bouché la sortie, la porte d'entrée non authentifiée est restée ouverte.

Un dépôt sans URL documentée devient une boîte noire dès qu'on change de machine. La vérif de sécurité s'est arrêtée là.

Captures d'écran