Retour aux projets

Portfolio mprost.fr — Conception, Sécurisation et Hébergement d'un site Next.js

2025-08-23
Next.js React TypeScript Tailwind CSS MDX Plesk Sécurité web Turnstile Journalisation RGPD SEO

Conception et mise en ligne de mon portfolio professionnel avec Next.js et MDX, hébergé sur Plesk. Le formulaire de contact est protégé côté serveur : CAPTCHA Cloudflare Turnstile, limitation de débit, honeypot, contrôle d'origine, secrets en variables d'environnement et journal de sécurité. Le projet intègre aussi la conformité RGPD et le suivi SEO.


Portfolio mprost.fr

Introduction

Ce site est mon portfolio professionnel. Il regroupe ma présentation, mon CV, mon parcours, mes documentations techniques et mes projets. Je l'ai développé moi-même, de la conception à l'hébergement, afin de maîtriser toute la chaîne : code, déploiement, sécurité et référencement.

Le site est volontairement léger et statique pour l'essentiel. Seul le formulaire de contact passe par une route serveur, ce qui m'a permis de travailler les enjeux de sécurité d'une application web exposée sur Internet : protection contre le spam et les abus, gestion des secrets, traçabilité des événements et protection des données personnelles.


Objectifs

  • Présenter mon profil, mon CV et mes réalisations dans un site unique et professionnel.
  • Publier mes documentations techniques et mes projets sans toucher au code : ajouter un fichier suffit.
  • Protéger le formulaire de contact contre les robots, le spam et l'épuisement de mon quota d'envoi.
  • Garder une trace des événements de sécurité pour détecter les abus et diagnostiquer les pannes.
  • Respecter le RGPD : peu de données collectées, information claire, aucun traceur publicitaire.
  • Suivre la visibilité et la performance du site (Google Search Console, Lighthouse).

Stack technique

DomaineTechnologiesRôle
FrameworkNext.js 16 (App Router), React 19, TypeScriptPages, routage, rendu serveur, route API
InterfaceTailwind CSS, Tailwind Typography, Framer Motion, LucideDesign, animations, icônes
Thèmenext-themesMode clair et sombre
ContenuMDX (next-mdx-remote), gray-matterProjets et documentations écrits en fichiers MDX avec métadonnées
Veillerss-parserComposant de veille technologique alimenté par des flux RSS
Anti-robotCloudflare TurnstileVérification que l'expéditeur du formulaire est un humain
Messagerie du formulaireFormspree (relais d'emails)Acheminement des messages reçus
HébergementPlesk, Node.jsExécution de l'application en production

Architecture

Organisation du contenu

Chaque projet ou documentation est un fichier .mdx placé dans le dossier content/projects. L'en-tête du fichier (titre, description, date, tags, liens) alimente automatiquement :

  • la carte affichée sur la page Projets ;
  • l'en-tête de la page de détail du projet ;
  • les pages statiques, générées au build à partir de la liste des fichiers.

Ajouter un projet ne demande donc aucune modification de code : un nouveau fichier suffit. Des composants React peuvent aussi être insérés dans le contenu, comme le composant de veille automatique.

Pages du site

  • Accueil, avec la section des compétences.
  • À propos, avec la formation et l'expérience.
  • Projets et Docs, alimentés par les fichiers MDX.
  • Contact, avec le formulaire.
  • Mentions légales et politique de confidentialité.

Parcours d'un message de contact

  1. Le visiteur remplit le formulaire et valide la vérification anti-robot Turnstile.
  2. La page envoie les données à la route serveur /api/contact de mon site, jamais directement à un service externe.
  3. La route applique ses contrôles, dans cet ordre : origine, limite par IP, validation des champs, honeypot, CAPTCHA, plafond global.
  4. Si tout est valide, le serveur relaie le message à Formspree, qui me le transmet par email.
  5. Chaque étape importante est consignée dans le journal de sécurité.

Sécurité

La sécurité a été pensée dès la conception, en particulier autour du formulaire de contact, seule partie du site qui reçoit des données de visiteurs.

Un formulaire protégé côté serveur

À l'origine, la page appelait directement Formspree depuis le navigateur. Cette méthode expose l'adresse du service : n'importe qui peut l'appeler en boucle sans passer par mon site, et consommer tout mon quota. Une limitation côté page n'aurait rien changé, puisqu'elle se contourne en appelant directement l'adresse.

J'ai donc déplacé l'envoi dans une route serveur (/api/contact). Le visiteur n'appelle que mon site, et c'est le serveur qui relaie le message. L'adresse du service est stockée dans une variable d'environnement serveur.

Mesures mises en place

RisqueMesure
Robots et envois automatisésCAPTCHA Cloudflare Turnstile, dont le jeton est vérifié côté serveur auprès de Cloudflare avant tout envoi
Envois répétés par une même personneLimitation par adresse IP : 3 messages maximum toutes les 10 minutes
Épuisement du quota d'envoiPlafond global de 20 messages par heure, toutes IP confondues, qui reste efficace même si une IP est falsifiée
Robots de spam simplesChamp piège invisible (honeypot) : si un robot le remplit, la requête est ignorée sans l'alerter
Appels depuis un autre siteVérification de l'en-tête Origin : seul mprost.fr est accepté
Exposition des secretsL'adresse du formulaire et la clé secrète Turnstile sont des variables d'environnement serveur, jamais préfixées NEXT_PUBLIC pour ne pas être envoyées au navigateur
Données invalides ou démesuréesValidation côté serveur : longueur maximale des champs, format de l'email, taille maximale du jeton
Panne du service anti-robotFonctionnement en échec fermé : si Cloudflare est injoignable ou trop lent, le message est refusé plutôt qu'accepté sans contrôle
Pages sans intérêt dans les moteurs de rechercheBalise noindex sur la politique de confidentialité
Interception des échangesConnexion HTTPS / TLS

Le CAPTCHA : pourquoi la vérification serveur est indispensable

Le widget Turnstile affiché dans la page ne protège rien à lui seul : un attaquant peut envoyer n'importe quelle chaîne de caractères à la route sans jamais passer par la page. Un jeton n'a de valeur que s'il est validé par le serveur auprès de Cloudflare, qui indique s'il est authentique. Un jeton est valable 5 minutes et ne peut servir qu'une seule fois. La page en redemande donc un nouveau après chaque envoi.

En production, le serveur vérifie aussi que le jeton a été émis pour le domaine mprost.fr.

Principe de la limitation de débit

Le serveur conserve, pour chaque IP, l'horodatage des dernières requêtes. À chaque envoi, il ne garde que celles qui sont dans la fenêtre de temps, puis compare leur nombre à la limite :

const recent = (hits.get(key) ?? []).filter((t) => now - t < windowMs);

if (recent.length >= limit) {
  // Trop de requêtes : code 429 et en-tête Retry-After
}

Lorsque la limite est atteinte, la route répond par un code 429 (Too Many Requests) avec un en-tête Retry-After. La page affiche alors un message invitant à patienter, distinct d'une erreur technique et distinct d'un échec du CAPTCHA.

Limites assumées

  • Les compteurs de limitation sont en mémoire : ils sont remis à zéro à chaque redémarrage de l'application. C'est suffisant pour un portfolio mono-instance.
  • L'en-tête Origin filtre les usages depuis un navigateur, mais un script peut le falsifier. C'est pour cela que le CAPTCHA, le plafond global et le honeypot complètent le dispositif.
  • La limite par IP dépend de la configuration du serveur web frontal, qui transmet l'adresse réelle du visiteur.
  • Un visiteur dont un bloqueur de contenu empêche le chargement de Turnstile ne peut pas envoyer de message. Son adresse email reste affichée sur la page de contact.

Journalisation et détection

Pour savoir ce qui se passe réellement sur le formulaire, la route enregistre chaque événement dans un journal.

Fonctionnement

  • Un fichier par jour : logs/contact-AAAA-MM-JJ.log.
  • Une ligne JSON par événement, avec la date, le type d'événement, l'adresse IP et le navigateur.
  • Les événements sont aussi écrits dans les logs Node.js de Plesk, avec le préfixe [contact].
  • Les fichiers de plus de 30 jours sont supprimés automatiquement.
  • Le dossier logs n'est pas dans le dossier public de Next.js, donc il n'est pas servi aux visiteurs.
  • Une erreur d'écriture du journal ne bloque jamais le formulaire.

Exemple de ligne, avec une adresse fictive :

"ts":"2026-10-17T15:42:10.000Z","event":"captcha_failed","ip":"203.0.113.7","codes":["timeout-or-duplicate"]

Événements consignés

ÉvénementSignificationUtilité
sentMessage transmis avec succèsVérifier le bon fonctionnement
rate_limited_ip / rate_limited_globalLimite atteinteRepérer une rafale d'envois
captcha_failed / captcha_missingVérification refusée ou absente, avec le code d'erreur CloudflareRepérer les robots et les appels directs à la route
captcha_unreachableCloudflare injoignableDiagnostiquer une panne du service anti-robot
honeypotUn robot a rempli le champ piègeMesurer le spam automatisé
bad_originAppel depuis un autre siteRepérer les tentatives de détournement
invalid_fields / invalid_bodyDonnées invalidesRepérer des requêtes malformées
formspree_error / formspree_unreachableProblème côté service d'emailsDiagnostiquer une panne d'acheminement
config_errorVariable d'environnement manquanteDiagnostiquer une erreur de configuration

Données consignées et vie privée

Une adresse IP étant une donnée personnelle, le journal est limité au strict nécessaire : le nom, l'adresse email et le contenu du message n'y figurent jamais. La durée de conservation est limitée à 30 jours, et la politique de confidentialité du site décrit ce traitement, sa finalité (protection contre les abus) et son prestataire anti-robot.

Pour garantir qu'une ligne de journal ne peut pas être fabriquée par un visiteur, chaque événement est sérialisé en JSON : les retours à la ligne ou guillemets injectés dans un en-tête comme le User-Agent sont neutralisés, et la longueur de ces champs est limitée.


Conformité RGPD

Le formulaire de contact est le seul traitement de données personnelles du site : nom, adresse email et message, auxquels s'ajoutent les données techniques du journal de sécurité.

  • Une politique de confidentialité détaille le responsable de traitement, les finalités, la base légale (intérêt légitime), les prestataires (Formspree et Cloudflare), les transferts hors UE et les droits des personnes.
  • Les messages sont conservés 12 mois au maximum, et le journal de sécurité 30 jours.
  • Le site n'utilise ni cookies publicitaires, ni outil d'analyse d'audience, ni pistage inter-sites.
  • Une case à cocher recueille l'accord de l'utilisateur avant l'envoi, avec un rappel d'information sous le formulaire qui mentionne la vérification anti-robot et le journal de sécurité.
  • La page de politique de confidentialité est exclue des moteurs de recherche, tout en restant accessible depuis le site.

Hébergement et déploiement

Le site est hébergé sur Plesk avec l'extension Node.js. Le déploiement suit ces étapes :

  1. Mise à jour du code et de la configuration (package.json, variables d'environnement).
  2. Installation des dépendances.
  3. Compilation avec le script build.
  4. Redémarrage de l'application.

Les secrets (adresse du formulaire, clé secrète Turnstile) sont fournis à l'application côté serveur. Ils ne sont écrits ni dans le code, ni dans le dépôt, et ne sont jamais envoyés au navigateur.

Problème rencontré : permission refusée sur le binaire Next.js

Au lancement, l'application échouait avec l'erreur next: Permission denied (code 126). Les fichiers du dossier node_modules/.bin avaient perdu leur droit d'exécution, ce qui arrive quand des fichiers sont transférés depuis un autre système.

Solution : appeler directement le fichier JavaScript de Next.js avec Node, sans passer par le binaire :

"build": "node ./node_modules/next/dist/bin/next build",
"start": "node ./node_modules/next/dist/bin/next start"

Cette correction contourne le problème de droits sans modifier le serveur. Elle m'a aussi appris à distinguer une erreur de code d'une erreur d'environnement : l'application était correcte, c'est l'exécution qui bloquait.

Problème rencontré : variable d'environnement non transmise

Après l'ajout de la route serveur, le formulaire renvoyait une erreur 500. La réponse de la route indiquait « Configuration serveur manquante » : l'application ne retrouvait pas l'adresse du formulaire, preuve que la variable d'environnement n'était pas transmise au processus Node.js.

Ce diagnostic a été rapide grâce à deux choix de conception : la route renvoie un code d'erreur précis plutôt qu'un échec générique, et elle consigne la cause dans le journal sans jamais afficher le secret au visiteur.


Référencement et performance

Le site est suivi avec deux outils :

  • Lighthouse, pour mesurer la performance, l'accessibilité, les bonnes pratiques et le SEO.
  • Google Search Console, pour suivre l'indexation et les performances dans les résultats de recherche.

Analyse Lighthouse du portfolio

Performances dans Google Search Console

Les métadonnées (titre, description) sont définies page par page. J'ai aussi corrigé un résultat parasite : la page de confidentialité apparaissait dans les moteurs de recherche avec le titre du site, ce qui a été réglé avec la balise noindex.


Compétences mobilisées

  • Développement web : Next.js, React, TypeScript, Tailwind CSS, MDX.
  • Sécurité applicative : CAPTCHA vérifié côté serveur, limitation de débit, validation des entrées, gestion des secrets, protection contre le spam.
  • Journalisation : conception d'un journal d'événements exploitable, sans donnée sensible, avec rotation automatique.
  • Hébergement et exploitation : Plesk, Node.js, variables d'environnement, diagnostic d'erreurs de déploiement.
  • Conformité : RGPD, politique de confidentialité, minimisation des données, durées de conservation.
  • Référencement : métadonnées, indexation, suivi des performances.

Pistes d'amélioration

  • Ajouter des en-têtes de sécurité (Content-Security-Policy, HSTS, X-Frame-Options) et vérifier le résultat avec un outil d'audit comme Mozilla Observatory. La Content-Security-Policy devra autoriser le domaine de Turnstile.
  • Stocker les compteurs de limitation dans une base persistante (Redis) pour qu'ils survivent aux redémarrages.
  • Centraliser et analyser les journaux (par exemple avec Wazuh ou Graylog) pour déclencher des alertes automatiques en cas de pic de tentatives.
  • Envoyer une alerte par email lorsqu'un seuil d'événements suspects est dépassé.