Projet CV en ligne
Le premier projet où l'on ne répond plus à un exercice, mais à un cahier des charges — et où l'on est jugé sur ce qu'on livre.
Résumé flash
Un site personnel en deux fichiers : cv.html pour la
structure, style.css pour la mise en forme. Cinq blocs imposés — header,
à propos, compétences, expériences, footer — chacun avec sa technique :
Flexbox pour aligner, Grid pour les cartes,
ancres pour naviguer. Le projet ne teste pas de nouvelle notion : il
vérifie que HTML et
CSS tiennent ensemble sur une vraie page.
🔑
#cahier-des-charges #CSS-externe #sémantique
#ancres #Flexbox #Grid #auto-fit
#minmax #formulaire #label-for
#media-query #responsive #alt
#données-personnelles
01Lire le cahier des charges
L'énoncé du projet n'est pas un texte à lire une fois : c'est la grille de notation, écrite à l'avance. Chaque puce est un point qu'on aura ou qu'on n'aura pas. Le premier geste du projet est donc de le retourner en check-list.
| Bloc | Ce qui est exigé | Technique attendue |
|---|---|---|
| 1. Header | nom et prénom · menu vers les 4 sections · image de profil (facultative) | Flexbox horizontal, fond et texte contrastés, espacement |
| 2. À propos | photo + 2 à 3 lignes de présentation | Flexbox : photo et texte côte à côte |
| 3. Compétences | 4 à 6 compétences techniques | CSS Grid, en cartes |
| 4. Expériences | pour chacune : titre · période · description | blocs verticaux, Flexbox ou Grid si l'écran le permet |
| 5. Footer | email · téléphone · LinkedIn ou GitHub | fond proche du header, contenu centré |
| Livrables | cv.html + style.css |
CSS externe, lié par <link> |
Le client commande « une entrée, un plat, un dessert ». Si le cuisinier envoie un plat magnifique mais oublie l'entrée, la table n'est pas servie — la qualité du plat ne rattrape pas l'oubli. En projet c'est identique : une section absente coûte plus cher qu'une section moyenne. On couvre tout d'abord, on embellit ensuite.
« Le fichier HTML doit inclure le CSS externe avec la balise
<link rel="stylesheet" href="style.css">. »
C'est une contrainte de notation, pas un conseil. Du CSS écrit dans un
<style> ou dans des attributs style="" ne remplit pas la
consigne, même si le rendu est identique. Le projet demande deux fichiers : il faut que le
correcteur puisse ouvrir style.css et y trouver toutes les règles.
L'énoncé écrit « Une petite icône ou image de profil (optionnel) ». En BTS, une mention facultative est presque toujours un point bonus, jamais un piège. Le coût est d'une ligne :
<img src="asset/pp.jpg" alt="Photo de profil de l'élève" class="avatar">Une ligne pour un point : on la met.
L'énoncé demande « 4 à 6 compétences techniques (HTML, CSS, Réseaux, Linux/Windows, etc.) ». Présenter Montage image et Comptabilité répond-il à la consigne ?
Partiellement. Le compte est bon — quatre cartes — mais deux d'entre elles ne sont pas des compétences techniques au sens de la formation. Le mot « techniques » et les exemples donnés (Réseaux, Linux/Windows) orientent vers le référentiel du BTS CIEL.
Le réflexe : garder ces cartes si elles vous distinguent — un CV n'est pas qu'une liste de langages — mais ajouter les compétences attendues, ici Réseaux & systèmes et Linux / Windows Server. On passe à six cartes, la consigne est couverte, et le CV gagne en cohérence avec la filière.
Règle générale : quand l'énoncé donne une fourchette (« 4 à 6 »), viser le haut de la fourchette coûte peu et sécurise la note.
02Poser le squelette
Avant la première règle CSS, on crée l'arborescence. Les chemins d'images sont écrits une fois dans le HTML et doivent rester valides sur la machine du correcteur.
projet-cv/
├── cv.html ← la structure
├── style.css ← toutes les règles de mise en forme
└── asset/ ← les images, dans un sous-dossier
├── pp.jpg (photo de profil)
├── background.jpg (bannière du header)
└── projet-1.png … projet-6.pngLe HTML écrit src="asset/pp.jpg" : un chemin relatif qui
suppose un sous-dossier asset/ à côté de cv.html. Si l'on rend un
dossier à plat — les trois fichiers et les images mélangés — ou si l'on n'envoie que
cv.html et style.css parce que « ce sont les deux livrables
demandés », toutes les images se cassent et la page paraît vide.
Le test : copier le dossier rendu sur le Bureau, sous un autre nom, l'ouvrir, et regarder. Ce que l'on voit là est exactement ce que le correcteur verra.
Les captures d'écran des projets s'appelaient projet (1).png,
projet (2).png… Cela fonctionne en local, mais une URL n'accepte ni espace ni
parenthèse : le navigateur doit les encoder en projet%20(1).png, et certains
serveurs refusent purement et simplement ces caractères. Le jour où le CV est mis en ligne,
les images disparaissent.
projet (1).png → projet-1.png. C'est la même règle de nommage que
pour les dossiers de ce site, et elle vaut pour tout fichier destiné au web.
La structure sémantique
Cinq blocs demandés, cinq conteneurs HTML. On choisit les balises pour ce qu'elles
signifient, pas pour leur apparence — c'est ce qui distingue une page
structurée d'une pile de <div>.
<body>
<header>
<nav>…le menu…</nav>
<div class="presentation">…nom et accroche…</div>
</header>
<main>
<section id="a-propos"> …</section>
<section id="competences">…</section>
<section id="experiences">…</section>
</main>
<footer id="contact">…</footer>
</body><footer> était à l'intérieur du <main>Dans la version rendue, la balise fermante </main> arrivait
après le footer :
<main>
<section id="a-propos">…</section>
…
<footer id="contact">…</footer> <!-- ✘ enfermé dans main -->
</main><main> désigne le contenu principal et unique de la
page — ce qui reste quand on retire l'en-tête, le menu et le pied de page. Un pied de page
à l'intérieur du contenu principal est une contradiction : un lecteur d'écran qui propose
« aller au contenu principal » y embarque le formulaire de contact, et la commande « aller
au pied de page » ne trouve plus rien au bon niveau.
La correction tient en un déplacement de la balise fermante : </main>
avant <footer>. Aucun pixel ne bouge à l'écran — et c'est justement pour
cela que l'erreur passe inaperçue jusqu'à la correction.
class="" videsLe fichier rendu contenait <nav class=""> et
<footer class="" id="contact">. Ce sont des restes d'édition : l'attribut
existe, ne vaut rien, ne sert à rien. Aucun effet sur le rendu, mais c'est le genre de
détail qu'un correcteur remarque — il montre que le fichier n'a pas été relu. Un
nettoyage de trente secondes avant de rendre.
03Le header et le menu
Le header porte trois choses : la bannière, le menu et l'accroche (nom + phrase d'introduction). Le menu est demandé horizontal : c'est un cas d'école pour Flexbox.
.menu {
display: flex; /* les <li> s'alignent en ligne */
justify-content: center; /* centrés horizontalement */
align-items: center; /* centrés verticalement */
gap: 30px; /* l'espacement demandé */
height: 50px;
margin: 0; padding: 0;
background: rgba(0, 0, 0, .55);
}
.menu li { list-style: none; }
.menu a { color: #fff; text-decoration: none; padding: 8px 12px; }
.menu a:hover { background: rgba(255, 255, 255, .18); border-radius: 5px; }Le menu pointe vers les sections par des ancres : un lien
href="#competences" fait défiler la page jusqu'à l'élément qui porte
id="competences". Aucun JavaScript, aucune page supplémentaire.
<a href="#competences">Compétences</a> <!-- 1. le lien : # + le nom -->
…
<section id="competences"> <!-- 2. la cible : id, sans # -->html { scroll-behavior: smooth; } /* le saut devient un glissement */
section { scroll-margin-top: 70px; } /* le titre ne passe pas sous le menu */scroll-margin-top règle le défaut classique du menu fixe : sans lui, le haut
de la section arrive exactement en haut de la fenêtre, donc caché derrière la
barre de navigation.
L'accroche contenait :
<button><a href="#experiences">Expériences</a></button> <!-- ✘ --><button> et <a> sont tous deux du contenu
interactif, et la spécification HTML interdit d'imbriquer l'un dans l'autre. Ce
n'est pas une chicane de puriste : le navigateur doit décider qui reçoit le clic, le
comportement diffère d'un navigateur à l'autre, et au clavier on obtient deux arrêts de
tabulation pour une seule action.
La question à se poser : est-ce que ça navigue, ou est-ce que ça agit ? Ici cela envoie vers une section — c'est donc un lien, qu'on habille en bouton :
<a class="bouton" href="#experiences">Voir mes projets</a>
.bouton {
display: inline-block; /* un lien accepte alors padding et taille */
padding: 12px 26px;
background: #2D5A27;
color: #fff;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
}<nav> enveloppait toute la bannièreDans la version rendue, <nav> contenait le menu et le bloc
.presentation (le nom, l'accroche, le bouton). Or <nav>
annonce : « ce qui suit est de la navigation ». Un lecteur d'écran propose de sauter les
zones de navigation — il sauterait donc aussi le nom du candidat, c'est-à-dire
l'information la plus importante de la page.
<header>
<nav><ul class="menu">…</ul></nav> <!-- uniquement les liens -->
<div class="presentation"> <!-- hors du nav -->
<h1>l'élève</h1>
<p>Étudiant en BTS CIEL — développement web et réseaux</p>
<a class="bouton" href="#experiences">Voir mes projets</a>
</div>
</header>Au passage : l'accroche rendue disait « Je suis moi ». C'est un texte de remplissage laissé en place. Sur un CV, c'est la deuxième ligne que lit un recruteur — elle doit annoncer la formation et le domaine.
li qui touche toute la pageli { /* ✘ TOUS les <li> du document */
margin-right: 15px;
margin-left: 15px;
list-style: none;
}La règle visait le menu, mais elle s'applique à chaque liste de la page — présentes et futures. Le jour où l'on ajoute une liste à puces dans « À propos », les puces disparaissent sans qu'on comprenne pourquoi, et la règle coupable est à cent lignes de là.
.menu li { margin: 0; list-style: none; } /* ✔ borné au menu */Principe : un sélecteur de balise nue (li, p,
img) n'a sa place que dans la section « base » d'une feuille de style, pour un
réglage volontairement global. Partout ailleurs, on préfixe par une classe.
Caler la bannière : hauteur fixe ou hauteur minimale ?
margin-bottom: 350px pour descendre l'accrocheheader { background-image: url("asset/background.jpg"); height: 700px; }
.menu { … margin-bottom: 350px; } /* ✘ pour pousser l'accroche vers le bas */Le résultat est correct sur l'écran où il a été réglé, et faux partout ailleurs. Deux défauts se combinent :
height: 700px— une hauteur fixe. Si le contenu devient plus grand (texte plus long, écran étroit où l'accroche passe sur trois lignes), il déborde du header et se superpose à la section suivante.margin-bottom: 350px— une marge choisie à l'œil pour compenser les 700 px. Changez la hauteur, tout est à refaire.
Flexbox fait le calcul lui-même, à n'importe quelle taille :
header {
min-height: 70vh; /* 70 % de la hauteur de fenêtre, jamais moins */
display: flex;
flex-direction: column;
justify-content: space-between; /* menu en haut, accroche en bas */
background-image: url("asset/background.jpg");
background-size: cover; /* l'image couvre tout le bloc */
background-position: center; /* sans déformation, recadrée au centre */
}min-height plutôt que height : la bannière ne sera jamais plus
petite que 70 % de l'écran, mais elle peut grandir si le contenu l'exige. C'est la
différence entre une contrainte et un carcan.
background-image sans background-sizePar défaut, une image de fond s'affiche à sa taille réelle et
se répète en mosaïque si le bloc est plus grand. On voit alors la même
photo carrelée trois fois. background-size: cover corrige les deux d'un coup :
l'image est mise à l'échelle pour couvrir la zone, le surplus est rogné.
| Valeur | Effet | Quand |
|---|---|---|
cover | couvre tout, rogne les bords | bannières, héros |
contain | image entière visible, bandes possibles | logo, illustration à ne pas couper |
| (défaut) | taille réelle, répétée | motif volontairement répété |
Deux lignes de la feuille rendue étaient sans effet — le genre de code mort qu'on accumule en essayant des choses :
.presentation { justify-self: center; }—justify-selfest une propriété de grille. Le parent était un bloc ordinaire : la déclaration est ignorée. Pour centrer un bloc de largeur connue :margin-inline: auto..menu > li > a:hover { margin: 10px; }— la règle normale posait déjàmargin: 10px. Au survol, on redéclare la même valeur : rien ne change. Dans un:hover, on n'écrit que ce qui varie.
04« À propos » en Flexbox
Consigne : « photo et texte côte à côte », en Flexbox. Trois déclarations suffisent.
.content-a-propos {
display: flex; /* les enfants se placent en ligne */
align-items: center; /* alignés sur leur milieu vertical */
gap: 28px; /* l'espace entre photo et texte */
}
.content-a-propos img {
width: 150px;
height: 150px;
object-fit: cover; /* recadre sans déformer */
border-radius: 50%; /* photo ronde */
}L'élève — étudiant en BTS CIEL, passionné par le web et les réseaux.
Je conçois des sites et j'administre des infrastructures.
height: 100% sur une image déforme.content-a-propos > div > img { height: 100%; width: 150px; } /* ✘ */Donner à la fois une largeur et une hauteur à une image l'étire ou l'écrase, sauf
coïncidence : la photo rendue, en 4:3, était comprimée dans un carré de 150 px. Et
height: 100% ne s'évalue que si le parent a une hauteur définie — ce qui
n'était pas le cas : la règle était donc à la fois nuisible et imprévisible.
deux dimensions → il faut
object-fit: coverobject-fit: cover agit sur une <img> comme
background-size: cover sur un fond : l'image remplit le cadre imposé, le
surplus est rogné, les proportions sont conservées.
Sur un écran de téléphone, la photo et le texte côte à côte donnent une colonne de texte de 120 px de large — illisible. Quelle est la correction, et pourquoi cette propriété plutôt qu'une media query ?
.content-a-propos {
display: flex;
flex-wrap: wrap; /* ← autorise le passage à la ligne */
gap: 28px;
}
.content-a-propos > div:last-child { flex: 1 1 280px; } /* base 280px, sinon on passe dessous */flex-wrap: wrap autorise les éléments à passer à la ligne quand ils ne
tiennent plus. Combiné à flex: 1 1 280px, le texte réclame au moins 280 px :
en dessous, il descend automatiquement sous la photo.
L'avantage sur une media query : le point de bascule n'est pas une largeur d'écran arbitraire, mais le moment réel où le contenu ne tient plus. Le comportement reste juste si l'on change la taille de la photo, la police, ou si le bloc est un jour placé dans une colonne plus étroite. La media query reste utile — mais pour ce qu'elle sait faire seule : réorganiser une page entière.
05« Compétences » en Grid
Ici l'énoncé impose l'outil : CSS Grid, en cartes. Flexbox aligne une file d'éléments ; Grid dessine un damier de lignes et de colonnes. Pour des cartes de même nature, c'est Grid.
.competences {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.competences .card {
background: #fff;
border-radius: 16px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .12);
text-align: center;
}
.competences .card h3 { margin-top: 0; color: #2D5A27; }Développement web
HTML, CSS, JavaScript, PHP, Bootstrap.
Réseaux
Cisco IOS, VLAN, routage, DHCP, NAT.
Systèmes
Linux Debian, Windows Server, VirtualBox.
Bases de données
MariaDB, SQL, PDO.
Pourquoi auto-fit plutôt que repeat(4, 1fr)
.competences { display: grid; grid-template-columns: repeat(4, 1fr); } /* ✘ */« Quatre colonnes » est une instruction que le navigateur applique à toutes les tailles d'écran. Sur un iPhone de 375 px de large, quatre cartes se partagent l'espace : environ 80 px chacune, marges déduites. Les titres se brisent lettre par lettre, les images débordent, la page défile horizontalement.
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));Cette ligne se lit ainsi : « fais autant de colonnes que tu peux, chacune d'au moins 220 px, et distribue l'espace restant à parts égales ». Le navigateur calcule le nombre de colonnes à partir de la place disponible :
| Largeur disponible | Colonnes obtenues | Appareil |
|---|---|---|
| 1000 px | 4 | ordinateur |
| 700 px | 3 | tablette paysage |
| 480 px | 2 | tablette portrait |
| 375 px | 1 | téléphone |
Une ligne de CSS, aucune media query, et la grille est responsive. C'est le motif le plus rentable de tout le CSS moderne : retenez-le tel quel.
minmax(220px, 1fr) — l'ordre des deux valeursminmax(min, max). Écrire minmax(1fr, 220px) est une erreur :
le minimum ne peut pas être une fraction de l'espace restant. La règle est alors ignorée et
la grille retombe sur une seule colonne — sans message d'erreur.
Et auto-fill n'est pas auto-fit : avec quatre cartes dans un
espace qui en contiendrait huit, auto-fill crée huit colonnes dont quatre
vides (les cartes restent à 220 px, serrées à gauche), tandis qu'auto-fit
replie les colonnes vides et laisse les quatre cartes s'étendre. Pour un CV,
auto-fit.
grid-template-rows: 150px 150px 150px.competences > .card {
display: grid;
grid-template-rows: 150px 150px 150px; /* ✘ titre / image / texte */
}L'intention était bonne : que les trois cartes s'alignent, titre sous titre, image sous image. Mais imposer 150 px à une rangée de texte suppose que le texte fera toujours cette hauteur. Le paragraphe de la première carte est trois fois plus long que celui de la troisième : il déborde de sa rangée et chevauche le bord de la carte.
Le second défaut est structurel : la carte « Testeur logiciel » enveloppait son paragraphe
dans un <div> supplémentaire, contrairement aux trois autres. Or
grid-template-rows distribue les enfants directs : la
structure devait être identique d'une carte à l'autre pour que l'alignement fonctionne.
.competences { align-items: stretch; } /* les cartes prennent la hauteur de la plus haute */
.competences .card {
display: grid;
grid-template-rows: auto auto 1fr; /* titre / image : leur taille — texte : le reste */
}auto laisse chaque rangée prendre la hauteur de son contenu ;
1fr donne tout l'espace restant à la dernière. Les cartes restent alignées, et
aucun texte n'est coupé.
Les cartes de compétences affichaient des illustrations pointant vers
fiverr-res.cloudinary.com, imgs.search.brave.com,
static-cse.canva.com et i.ytimg.com. Quatre problèmes
superposés :
- Le CV ne fonctionne pas hors connexion. Ouvert sans réseau — en salle d'examen, dans le train — il montre quatre cadres vides.
- Les liens meurent. Ces URL sont des adresses internes de services tiers (une annonce Fiverr, un cache de moteur de recherche). Elles changent sans préavis. Un CV qui affiche des images cassées six mois plus tard est pire qu'un CV sans images.
- Ce sont les images de quelqu'un d'autre. Les afficher depuis son site, sans licence, est une contrefaçon — et charger l'image depuis le serveur d'autrui lui fait payer la bande passante. Le terme consacré est hotlinking.
- Ça sort du livrable. L'énoncé demande
cv.htmletstyle.css: le projet doit être autonome.
La correction : télécharger des visuels libres de droits
(Unsplash, Pexels, icônes Feather ou Bootstrap Icons), les placer dans
asset/, et pointer dessus en relatif.
<img src="asset/icone-web.svg" alt="" width="72" height="72">Un SVG est ici idéal : quelques kilo-octets, net à toutes les tailles et
sur les écrans Retina. Et l'alt vide est correct dans ce cas précis :
l'image est décorative, le titre de la carte porte déjà l'information — la répéter
encombrerait le lecteur d'écran. Voir la section 9.
Faute de frappe dans un titre de carte — Testeur. Un correcteur la voit en une seconde, un recruteur aussi, et sur un CV dont l'objet est justement la qualité logicielle elle tombe particulièrement mal. Même remarque pour « Site Mamma clazon » (calzone) et « projets web interactives » (interactifs : accord avec projets).
Le réflexe : copier le texte du CV dans un traitement de texte, laisser le correcteur orthographique passer, recopier. Deux minutes, et cela vaut plus qu'une heure de CSS.
06« Projets » et les images
Chaque expérience doit porter trois informations : titre,
période, description. La balise
<article> convient mieux qu'un <div> : chaque projet est
un contenu autonome, qui garderait son sens sorti de la page.
<article class="card">
<h3>SpeedrunHub</h3>
<time datetime="2023-11">Novembre 2023</time>
<img src="asset/projet-2.png" alt="Page d'accueil de SpeedrunHub" loading="lazy">
<p>Plateforme communautaire dédiée au speedrunning : partage de performances,
forums, profils de jeux et gestion des utilisateurs.</p>
</article><time> — la date lisible par la machineLa version rendue écrivait <p>Date : Janvier 2021</p>. Correct,
mais c'est du texte : rien n'indique qu'il s'agit d'une date.
<time datetime="2021-01">Janvier 2021</time>L'attribut datetime porte la date au format normalisé
AAAA-MM-JJ ; le texte visible reste libre. Les moteurs de recherche et les
robots d'analyse de CV savent alors trier les expériences par date.
height: 405px; width: 650px sur les captures.card-image-projets > img { height: 405px; width: 650px; } /* ✘ */650 px imposés à une image placée dans une carte de 375 px : elle déborde, et la page entière défile horizontalement — le défaut le plus visible d'un site non responsive. Par ailleurs, les six captures n'ont pas toutes le même format : les forcer en 650 × 405 en déforme certaines.
.card-image-projets img {
width: 100%; /* jamais plus large que sa carte */
aspect-ratio: 16 / 10; /* toutes les vignettes au même format */
object-fit: cover; /* recadrées, pas déformées */
border-radius: 10px;
display: block; /* supprime l'espace fantôme sous l'image */
}pas en pixels.
background.jpg pèse 8 Mo. C'est la photo sortie de l'appareil,
glissée telle quelle dans le dossier. Conséquences : plusieurs secondes d'écran vide en
4G, et le quota de données du visiteur entamé pour une seule image de fond.
| Usage | Largeur utile | Poids visé |
|---|---|---|
| Bannière pleine largeur | 1920 px | < 300 Ko |
| Capture de projet | 800 px | < 150 Ko |
| Photo de profil | 400 px | < 60 Ko |
| Icône | SVG | < 10 Ko |
Redimensionner puis exporter en WebP (ou en JPEG de qualité 80) fait
tomber une photo de 8 Mo sous les 200 Ko sans différence visible à l'écran. Et
loading="lazy" sur les images de projets diffère leur chargement jusqu'à ce
qu'on défile jusqu'à elles.
« Plus ou Moins », « Application Météo » et « Janken de luxe » portent tous Janvier 2021. C'est probablement exact — trois petits projets menés dans la même période d'apprentissage — mais un lecteur y voit une date recopiée par facilité.
Deux options, aussi honnêtes l'une que l'autre : dater précisément chaque projet, ou les regrouper sous un intitulé commun (« Projets d'initiation — JavaScript, janvier 2021 ») qui explique la simultanéité au lieu de la subir.
07Le formulaire de contact
L'énoncé ne demandait qu'un bloc d'informations de contact. Le projet rendu va plus loin : il ajoute un formulaire. C'est une bonne initiative — et c'est aussi l'endroit du projet qui concentre le plus d'erreurs, parce qu'un formulaire s'affiche correctement même quand il est faux.
<form method="post" action="contact.php">
<fieldset>
<legend>Me contacter</legend>
<label for="nom">Nom</label>
<input type="text" id="nom" name="nom" required>
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<label for="telephone">Téléphone</label>
<input type="tel" id="telephone" name="telephone">
<label for="message">Votre message</label>
<textarea id="message" name="message" rows="5" required></textarea>
<button type="submit">Envoyer</button>
</fieldset>
</form><form action="POST">C'est l'erreur la plus fréquente du chapitre formulaires, et elle mérite qu'on s'y arrête : les deux attributs répondent à deux questions différentes.
| Attribut | Question | Valeur |
|---|---|---|
action | Où j'envoie ? | une URL : contact.php |
method | Comment j'envoie ? | get ou post |
Écrire action="POST" revient à donner le mot « POST » comme adresse de
destination. Le navigateur cherche un fichier nommé POST à côté de
cv.html, ne le trouve pas — et comme method est absent, il
applique la valeur par défaut, get : les champs partent dans la barre
d'adresse. Rien ne signale l'erreur, sinon une page 404 après le clic.
Le rappel complet sur GET et POST est au
chapitre PHP — sessions.
<label> qui ne pointaient sur rien<label for="first-name">Email :</label>
<input type="email" id="email" name="email" required>
<label for="first-name">Téléphone :</label>
<input type="number" id="number" name="number" required>Le for d'un label doit reprendre exactement l'id
du champ. Ici for="first-name" — vestige d'un copier-coller — ne correspond à
aucun id de la page, et il est répété deux fois. Les deux labels sont
donc orphelins :
- cliquer sur le mot « Email » ne place plus le curseur dans le champ ;
- un lecteur d'écran annonce « zone de saisie, sans intitulé » — l'utilisateur ne sait pas quoi taper ;
- la zone cliquable, précieuse au doigt sur un téléphone, se réduit au champ lui-même.
<label for="email">Email</label>
<input type="email" id="email" name="email" required>Le test en trois secondes, sans aucun outil : cliquer sur chaque intitulé. Si le curseur ne saute pas dans le champ, la liaison est cassée.
type="number" pour un numéro de téléphoneUn numéro de téléphone n'est pas un nombre : c'est une suite de chiffres. La différence est loin d'être théorique.
Avec type="number" | Avec type="tel" |
|---|---|
le 0 initial de 07 22… peut être perdu |
la saisie est conservée telle quelle |
espaces, +33 et points refusés |
tous les formats acceptés |
| des flèches d'incrément s'affichent | aucun contrôle parasite |
| la molette modifie le numéro par accident | la molette fait défiler la page |
| clavier numérique avec virgule et signe moins | clavier téléphone sur mobile |
<input type="tel" id="telephone" name="telephone"
placeholder="06 12 34 56 78"
pattern="[0-9 +().-]{6,20}">type="number" est réservé aux valeurs sur lesquelles une addition a du sens :
une quantité, un âge, un prix. Jamais un téléphone, un code postal, un numéro de sécurité
sociale ni un numéro de carte.
<textarea> sans intituléLa zone de message n'avait ni <label> ni placeholder : un
grand rectangle vide, sans indication de ce qu'on attend. Et required y était
absent alors qu'il figurait sur tous les autres champs — on pouvait donc envoyer un message
vide, ce qui est précisément l'inverse du besoin.
Un placeholder ne remplace jamais un <label> : il disparaît
dès la première frappe, son contraste est faible par défaut, et il n'est pas lu comme un
intitulé par les technologies d'assistance.
Même corrigé, ce formulaire ne fait rien : HTML et CSS s'exécutent chez le
visiteur, et rien chez le visiteur ne peut envoyer un courriel. Il faut un
script côté serveur — le fichier contact.php désigné par
action — qui reçoive $_POST, valide les champs et appelle
mail(). C'est exactement le mécanisme du
chapitre PHP.
Sur un hébergement purement statique (GitHub Pages, par exemple), PHP n'existe pas. La solution honnête est alors d'assumer un lien direct :
<a href="mailto:prenom.nom@exemple.fr">Écrivez-moi</a>Un formulaire qui ne part nulle part est pire que pas de formulaire du tout : le visiteur croit vous avoir écrit.
Le bouton du formulaire portait le texte « Envoyé ». Pourquoi « Envoyer » est-il la bonne étiquette ?
Parce qu'un bouton nomme l'action qu'il va déclencher, pas un état atteint. « Envoyé » est un participe passé : il annonce que le message est déjà parti, alors qu'on n'a pas encore cliqué. C'est une promesse fausse, et au lecteur d'écran cela s'annonce « bouton — Envoyé », ce qui est franchement trompeur.
Règle d'ergonomie : un bouton se nomme à l'infinitif — Envoyer, Valider, Supprimer, Télécharger. Le participe passé (« Envoyé ✓ ») ne s'affiche qu'après, dans le message de confirmation.
08Rendre le site responsive
Le projet rendu ne contient aucune media query. Sur un téléphone, le menu déborde, les quatre cartes de compétences font 80 px de large et les captures de projets forcent un défilement horizontal. Or un CV en ligne est très souvent ouvert depuis un téléphone.
- La balise viewport — sans elle, rien d'autre ne sert à rien :
htmlElle était bien présente dans le projet. Sans elle, un téléphone fait semblant de mesurer 980 px et dézoome la page entière.
<meta name="viewport" content="width=device-width, initial-scale=1.0"> - Des dimensions souples —
%,fr,minmax(),max-widthplutôt que des pixels figés. - Des media queries pour ce que le contenu ne peut pas régler seul : réorganiser la page.
@media (max-width: 768px) {
.menu { flex-wrap: wrap; height: auto; padding: 10px 0; gap: 10px; }
header { min-height: 55vh; }
.fiche-tete h1 { font-size: 1.8rem; }
.content-a-propos { flex-direction: column; text-align: center; }
fieldset { padding: 20px; } /* 60px de padding sur 375px : il ne reste rien */
}padding: 60px du formulaireSur un écran de 375 px, un padding: 60px à gauche et à droite consomme 120 px :
il reste 255 px pour les champs. Combiné à box-sizing non réglé, la boîte
déborde du parent et la page défile horizontalement.
Le réflexe universel, à placer en tête de toute feuille de style :
*, *::before, *::after { box-sizing: border-box; }Padding et bordure sont alors compris dans la largeur annoncée : un bloc en
width: 100% reste large de 100 %, quel que soit son padding.
Dans le navigateur : F12 (ou ⌥⌘I sur Mac), puis l'icône de téléphone — Toggle device toolbar. Choisir iPhone SE (375 px), la largeur la plus contraignante encore courante.
Si elle apparaît, un élément est trop large. Pour le trouver en une ligne, dans la console :
document.querySelectorAll('*').forEach(e => {
if (e.getBoundingClientRect().right > document.documentElement.clientWidth)
console.log(e); // affiche le ou les coupables
});09Accessibilité et vie privée
Les attributs alt
Toutes les images du projet portaient alt="". Un alt vide n'est pas
une erreur en soi — c'est même la bonne réponse pour une image décorative — mais il doit être
un choix, pas un oubli.
| Image | alt attendu | Pourquoi |
|---|---|---|
| Photo de profil | alt="l'élève" |
elle porte une information : qui est la personne |
| Capture d'un projet | alt="Page d'accueil de SpeedrunHub" |
elle montre le travail décrit à côté |
| Icône de compétence | alt="" |
décorative : le titre de la carte dit déjà tout |
| Fond de bannière | en CSS | purement décoratif : jamais dans le HTML |
L'accroche est en blanc sur rgba(0,0,0,.685), posé sur une photographie. Le
contraste dépend alors de la zone de l'image qui se trouve derrière — il est excellent sur
une partie sombre, insuffisant sur un ciel clair.
Le critère WCAG AA demande un rapport de 4,5:1 pour le texte courant. Pour le garantir quelle que soit la photo, on superpose un voile uniforme :
header {
background-image:
linear-gradient(rgba(0,0,0,.55), rgba(0,0,0,.55)), /* le voile, au-dessus */
url("asset/background.jpg"); /* la photo, en dessous */
background-size: cover;
}L'ordre compte : dans background-image, la première couche
déclarée est celle du dessus.
Ce qu'on met — et ne met pas — sur un CV public
Le bloc « Mes renseignements » affichait un numéro de téléphone et une adresse postale complète. Mis en ligne, ce bloc est aspiré par les moteurs de recherche et par les robots collecteurs d'adresses, et reste consultable des années dans les caches — bien après la mise à jour de la page.
C'est exactement le matériau de l'OSINT étudié au chapitre cybersécurité : nom, téléphone, adresse, employeur et photo réunis sur une seule page suffisent à monter une usurpation d'identité ou un hameçonnage ciblé crédible.
| Donnée | Sur un CV public | Sur un CV envoyé en PDF |
|---|---|---|
| Nom et prénom | ✔ oui | ✔ oui |
| ✔ oui — une adresse dédiée | ✔ oui | |
| LinkedIn / GitHub | ✔ oui | ✔ oui |
| Ville | ✔ oui — « Trappes (78) » suffit | ✔ oui |
| Adresse postale | ✘ non | ✔ si demandé |
| Téléphone | ✘ non — ou via le formulaire | ✔ oui |
| Date de naissance | ✘ non | facultatif |
Le principe du RGPD dit minimisation : ne publier que ce qui est nécessaire à la finalité. La finalité d'un CV en ligne est qu'un recruteur puisse vous joindre — un email y suffit.
Le projet affichait une adresse email et une adresse postale manifestement fantaisistes. Pour un TP c'est le bon réflexe : le fichier circule, il est corrigé, parfois republié — mieux vaut qu'il ne contienne aucune donnée réelle.
À une réserve près : le lien LinkedIn, lui, était bien réel. Un fichier ne peut pas être à moitié anonyme. Sur un rendu de TP, ou bien toutes les coordonnées sont fictives, ou bien ce sont celles qu'on assume de publier — pas un mélange des deux.
10Relire et livrer
Le projet est écrit. Reste le geste qui distingue un rendu correct d'un bon rendu : la relecture. Elle se fait dans cet ordre, sans en sauter.
- Le cahier des charges, ligne à ligne. Reprendre le tableau de la section 1 et cocher. Les cinq blocs sont-ils là ? Le CSS est-il bien externe ?
- Le dossier copié ailleurs. Dupliquer le projet sur le Bureau, ouvrir la copie : toutes les images s'affichent-elles ?
- Le validateur W3C.
validator.w3.org, onglet Validate by File Upload. Il détecte en une seconde le<a>dans un<button>, les balises mal fermées et lesiddupliqués. - La console du navigateur (F12). Zéro erreur rouge. Une image manquante
y apparaît en
404même quand la page semble correcte. - La largeur 375 px. Aucune barre de défilement horizontale.
- Chaque intitulé de formulaire cliqué. Le curseur saute-t-il dans le bon champ ?
- L'orthographe. Le texte relu dans un correcteur, titres compris.
Un enseignant qui corrige trente projets ouvre la page, regarde dix secondes, puis ouvre le code. Dans ces dix secondes se joue l'essentiel :
- la page s'affiche — pas de cadres vides ni d'images cassées ;
- les cinq sections demandées sont visibles ;
- les liens du menu fonctionnent ;
- elle tient sur un téléphone.
Puis, dans le code : le CSS est-il bien externe, les balises sont-elles sémantiques, les classes nommées lisiblement. C'est dans cet ordre qu'il faut relire soi-même.
C'est la première brique du dossier E4. Un CV en ligne qui fonctionne vraiment — hébergé, responsive, à jour — est une adresse à donner en entretien d'alternance, et la démonstration la plus directe qu'on sait livrer quelque chose de fini.
Le pas suivant coûte une heure : déposer le dossier sur un dépôt GitHub et activer GitHub Pages. Le site devient public à une vraie adresse, gratuitement, et l'historique du dépôt montre le travail — ce qu'un fichier joint à un courriel ne montrera jamais.
Le CV s'affiche parfaitement sur votre ordinateur. Chez le correcteur, les images des projets ne s'affichent pas, mais les icônes de compétences si. Quelle est la cause la plus probable ?
Les images locales n'ont pas été rendues, contrairement aux images distantes.
Les icônes de compétences venaient d'URL en https:// : elles se chargent
depuis Internet, quel que soit le dossier. Les captures de projets, elles, étaient en
chemin relatif asset/projet-1.png — si le dossier asset/ n'est
pas dans le rendu, elles sont introuvables.
Deuxième cause possible, plus sournoise : la casse. macOS et Windows ne
distinguent pas majuscules et minuscules dans les noms de fichiers ; Linux — et donc la
quasi-totalité des serveurs web — les distingue. Un src="asset/PP.jpg" qui
désigne un fichier pp.jpg fonctionne chez vous et nulle part ailleurs.
Les deux causes se préviennent de la même façon : tout en minuscules, sans espace ni accent, et un test sur une copie du dossier avant de rendre.